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(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 });
116 /** List of service implementation for each accessory type. **/
117 private static final Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<>() {
119 put(ACCESSORY_GROUP, HomekitAccessoryGroupImpl.class);
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);
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())));
166 if (taggedItem.getAccessoryType() == BATTERY) {
167 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
169 characteristics.add(BATTERY_CHARGING_STATE);
172 return characteristics;
176 * creates HomeKit accessory for an openhab item.
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
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
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);
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<>();
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");
209 AbstractHomekitAccessoryImpl accessoryImpl;
211 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
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");
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;
231 LOGGER.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
232 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
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);
242 * return HomeKit accessory types for an OH item based on meta data
244 * @param item OH item
245 * @param metadataRegistry meta data registry
246 * @return list of HomeKit accessory types and characteristics.
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));
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)));
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;
281 * return list of HomeKit relevant groups linked to an accessory
283 * @param item OH item
284 * @param itemRegistry item registry
285 * @param metadataRegistry metadata registry
286 * @return list of relevant group items
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);
295 return Stream.empty();
297 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
301 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
304 * @param taggedItem HomeKit tagged item
305 * @param metadataRegistry meta data registry
306 * @return list of mandatory
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);
316 addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, taggedItem.getItem(),
319 LOGGER.trace("Mandatory characteristics: {}, {}", characteristics, rawCharacteristics);
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
329 * @param mainItem main item
330 * @param characteristics list of characteristics
331 * @param item current item
332 * @param metadataRegistry meta date registry
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
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,
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))));
359 // item has characteristic tag on it, so, adding it as that characteristic.
361 final HomekitCharacteristicType characteristic = accessory.getValue();
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)));
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) {
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(),
385 characteristic.ifPresent(rc -> rawCharacteristics.add(rc));
392 * add optional characteristics for given accessory.
394 * @param taggedItem main item
395 * @param accessory accessory
396 * @param metadataRegistry metadata registry
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) -> {
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());
425 * add optional characteristics for given accessory from metadata
427 * @param taggedItem main item
428 * @param accessory accessory
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) {
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(),
442 if (characteristic.isPresent()) {
443 accessory.addCharacteristic(characteristic.get());
449 * creates HomeKit services for an openhab item that are members of this group item.
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
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
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)) {
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());
476 LOGGER.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
477 if (accessoryTypes.isEmpty()) {
481 if (accessoryTypes.size() > 1) {
482 LOGGER.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
483 groupMember.getName());
487 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
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();
496 if (subAccessory.isLinkable(accessory)) {
497 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
499 accessory.getServices().add(subAccessory.getPrimaryService());
505 * collect optional HomeKit characteristics for a OH item.
507 * @param taggedItem main OH item
508 * @param metadataRegistry OH metadata registry
509 * @return a map with characteristics and corresponding OH items
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)));
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()));
526 LOGGER.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
527 return Collections.unmodifiableMap(characteristicItems);
531 * return true is characteristic is a mandatory characteristic for the accessory.
534 * @param characteristic characteristic
535 * @return true if characteristic is mandatory, false if not mandatory
537 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
538 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
539 && getRequiredCharacteristics(item).contains(characteristic);
543 * check whether accessory is root accessory, i.e. without characteristic tag.
545 * @param accessory accessory
546 * @return true if accessory has not characteristic.
548 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
549 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
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.
556 * @param accessoryType accessory type
557 * @param characteristic characteristic
558 * @return true if characteristic belongs to the accessory type.
560 private static boolean belongsToType(HomekitAccessoryType accessoryType,
561 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
562 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));