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, new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE });
83 put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE,
84 TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE });
85 put(HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
86 put(INPUT_SOURCE, new HomekitCharacteristicType[] {});
87 put(IRRIGATION_SYSTEM, new HomekitCharacteristicType[] { ACTIVE, INUSE_STATUS, PROGRAM_MODE });
88 put(LEAK_SENSOR, new HomekitCharacteristicType[] { LEAK_DETECTED_STATE });
89 put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
90 put(LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
91 put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
92 put(MICROPHONE, new HomekitCharacteristicType[] { MUTE });
93 put(MOTION_SENSOR, new HomekitCharacteristicType[] { MOTION_DETECTED_STATE });
94 put(OCCUPANCY_SENSOR, new HomekitCharacteristicType[] { OCCUPANCY_DETECTED_STATE });
95 put(OUTLET, new HomekitCharacteristicType[] { ON_STATE, INUSE_STATUS });
97 new HomekitCharacteristicType[] { SECURITY_SYSTEM_CURRENT_STATE, SECURITY_SYSTEM_TARGET_STATE });
98 put(SMART_SPEAKER, new HomekitCharacteristicType[] { CURRENT_MEDIA_STATE, TARGET_MEDIA_STATE });
99 put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
100 put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
101 put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
102 put(STATELESS_PROGRAMMABLE_SWITCH, new HomekitCharacteristicType[] { PROGRAMMABLE_SWITCH_EVENT });
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(STATELESS_PROGRAMMABLE_SWITCH, HomekitStatelessProgrammableSwitchImpl.class);
149 put(SWITCH, HomekitSwitchImpl.class);
150 put(TELEVISION, HomekitTelevisionImpl.class);
151 put(TELEVISION_SPEAKER, HomekitTelevisionSpeakerImpl.class);
152 put(TEMPERATURE_SENSOR, HomekitTemperatureSensorImpl.class);
153 put(THERMOSTAT, HomekitThermostatImpl.class);
154 put(VALVE, HomekitValveImpl.class);
155 put(WINDOW, HomekitWindowImpl.class);
156 put(WINDOW_COVERING, HomekitWindowCoveringImpl.class);
160 private static List<HomekitCharacteristicType> getRequiredCharacteristics(HomekitTaggedItem taggedItem) {
161 final List<HomekitCharacteristicType> characteristics = new ArrayList<>();
162 if (MANDATORY_CHARACTERISTICS.containsKey(taggedItem.getAccessoryType())) {
163 characteristics.addAll(Arrays.asList(MANDATORY_CHARACTERISTICS.get(taggedItem.getAccessoryType())));
165 if (taggedItem.getAccessoryType() == BATTERY) {
166 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
168 characteristics.add(BATTERY_CHARGING_STATE);
171 return characteristics;
175 * creates HomeKit accessory for an openhab item.
177 * @param taggedItem openhab item tagged as HomeKit item
178 * @param metadataRegistry openhab metadata registry required to get item meta information
179 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
181 * @param settings OH settings
182 * @return HomeKit accessory
183 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
186 public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
187 HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
188 Set<HomekitTaggedItem> ancestorServices = new HashSet<>();
189 return create(taggedItem, metadataRegistry, updater, settings, ancestorServices);
192 @SuppressWarnings("null")
193 private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
194 HomekitAccessoryUpdater updater, HomekitSettings settings, Set<HomekitTaggedItem> ancestorServices)
195 throws HomekitException {
196 final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
197 LOGGER.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
198 final List<HomekitTaggedItem> characteristics = new ArrayList<>();
199 final List<Characteristic> rawCharacteristics = new ArrayList<>();
201 getMandatoryCharacteristicsFromItem(taggedItem, metadataRegistry, characteristics, rawCharacteristics);
202 final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
203 if (characteristics.size() + rawCharacteristics.size() < mandatoryCharacteristics.size()) {
204 LOGGER.warn("Accessory of type {} must have following characteristics {}. Found only {}, {}",
205 accessoryType.getTag(), mandatoryCharacteristics, characteristics, rawCharacteristics);
206 throw new HomekitException("Missing mandatory characteristics");
208 AbstractHomekitAccessoryImpl accessoryImpl;
210 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
212 if (accessoryImplClass != null) {
213 if (ancestorServices.contains(taggedItem)) {
214 LOGGER.warn("Item {} has already been created. Perhaps you have circular Homekit accessory groups?",
215 taggedItem.getName());
216 throw new HomekitException("Circular accessory references");
218 accessoryImpl = accessoryImplClass
219 .getConstructor(HomekitTaggedItem.class, List.class, List.class, HomekitAccessoryUpdater.class,
220 HomekitSettings.class)
221 .newInstance(taggedItem, characteristics, rawCharacteristics, updater, settings);
222 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
223 addOptionalMetadataCharacteristics(taggedItem, accessoryImpl);
224 accessoryImpl.setIsLinkedService(!ancestorServices.isEmpty());
225 accessoryImpl.init();
226 ancestorServices.add(taggedItem);
227 addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
228 return accessoryImpl;
230 LOGGER.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
231 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
233 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
234 | InvocationTargetException e) {
235 LOGGER.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
236 throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
241 * return HomeKit accessory types for an OH item based on meta data
243 * @param item OH item
244 * @param metadataRegistry meta data registry
245 * @return list of HomeKit accessory types and characteristics.
247 public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
248 MetadataRegistry metadataRegistry) {
249 final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
250 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
251 if (metadata != null) {
252 String[] tags = metadata.getValue().split(",");
253 for (String tag : tags) {
254 final String[] meta = tag.split("\\.");
255 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
256 if (accessoryType.isPresent()) { // it accessory, check for characteristic
257 HomekitAccessoryType type = accessoryType.get();
258 if (meta.length > 1) {
259 // it has characteristic as well
260 accessories.add(new SimpleEntry<>(type, Objects
261 .requireNonNull(HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY))));
262 } else {// it has no characteristic
263 accessories.add(new SimpleEntry<>(type, EMPTY));
265 } else { // it is no accessory, so, maybe it is a characteristic
266 HomekitCharacteristicType.valueOfTag(meta[0].trim())
267 .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
274 public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
275 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
276 return metadata != null ? metadata.getConfiguration() : null;
280 * return list of HomeKit relevant groups linked to an accessory
282 * @param item OH item
283 * @param itemRegistry item registry
284 * @param metadataRegistry metadata registry
285 * @return list of relevant group items
287 public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
288 MetadataRegistry metadataRegistry) {
289 return item.getGroupNames().stream().flatMap(name -> {
290 final @Nullable Item itemFromRegistry = itemRegistry.get(name);
291 if (itemFromRegistry instanceof GroupItem groupItem) {
292 return Stream.of(groupItem);
294 return Stream.empty();
296 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
300 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
303 * @param taggedItem HomeKit tagged item
304 * @param metadataRegistry meta data registry
305 * @return list of mandatory
307 private static void getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
308 MetadataRegistry metadataRegistry, List<HomekitTaggedItem> characteristics,
309 List<Characteristic> rawCharacteristics) {
310 if (taggedItem.isGroup()) {
311 for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
312 addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, item, metadataRegistry);
315 addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, taggedItem.getItem(),
318 LOGGER.trace("Mandatory characteristics: {}, {}", characteristics, rawCharacteristics);
322 * add mandatory HomeKit items for a given main item to a list of characteristics.
323 * Main item is use only to determine, which characteristics are mandatory.
324 * The characteristics are added to item.
325 * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
328 * @param mainItem main item
329 * @param characteristics list of characteristics
330 * @param item current item
331 * @param metadataRegistry meta date registry
333 private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
334 List<Characteristic> rawCharacteristics, Item item, MetadataRegistry metadataRegistry) {
335 // get list of mandatory characteristics
336 List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
337 if (mandatoryCharacteristics.isEmpty()) {
338 // no mandatory characteristics linked to accessory type of mainItem. we are done
341 // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
342 // if we are adding not to the main item (typical for groups), create new proxy item.
343 final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
344 : new HomekitOHItemProxy(item);
345 // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
346 for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
348 // if the item has only accessory tag, e.g. TemperatureSensor,
349 // then we will link all mandatory characteristic to this item,
350 // e.g. we will link CurrentTemperature in case of TemperatureSensor.
351 // Note that accessories that are members of other accessories do _not_
352 // count - we're already constructing another root accessory.
353 if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
354 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
355 accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
356 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
358 // item has characteristic tag on it, so, adding it as that characteristic.
360 final HomekitCharacteristicType characteristic = accessory.getValue();
362 // check whether it is a mandatory characteristic. optional will be added later by another method.
363 if (belongsToType(mainItem.getAccessoryType(), accessory)
364 && isMandatoryCharacteristic(mainItem, characteristic)) {
365 characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
366 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
367 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
371 mandatoryCharacteristics.forEach(c -> {
372 // Check every metadata key looking for a characteristics we can create
373 var config = mainItem.getConfiguration();
374 if (config == null) {
377 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
378 .collect(Collectors.toList())) {
379 var type = HomekitCharacteristicType.valueOfTag(entry.getKey());
380 if (type.isPresent() && isMandatoryCharacteristic(mainItem, type.get())) {
381 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(type.get(),
384 characteristic.ifPresent(rc -> rawCharacteristics.add(rc));
391 * add optional characteristics for given accessory.
393 * @param taggedItem main item
394 * @param accessory accessory
395 * @param metadataRegistry metadata registry
397 private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
398 MetadataRegistry metadataRegistry) {
399 Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
400 accessory.getRootAccessory(), metadataRegistry);
401 HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
402 proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
403 // an accessory can have multiple optional characteristics. iterate over them.
404 characteristics.forEach((type, item) -> {
406 // check whether a proxyItem already exists, if not create one.
407 final HomekitOHItemProxy proxyItem = Objects
408 .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
409 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
410 accessory.getRootAccessory().getAccessoryType(), type,
411 accessory.getRootAccessory().getRootDeviceGroupItem(),
412 getItemConfiguration(item, metadataRegistry));
413 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
414 accessory.getUpdater());
415 accessory.addCharacteristic(optionalItem, characteristic);
416 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
417 LOGGER.warn("Unsupported optional HomeKit characteristic: type {}, characteristic type {}",
418 accessory.getPrimaryService(), type.getTag());
424 * add optional characteristics for given accessory from metadata
426 * @param taggedItem main item
427 * @param accessory accessory
429 private static void addOptionalMetadataCharacteristics(HomekitTaggedItem taggedItem,
430 AbstractHomekitAccessoryImpl accessory)
431 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, HomekitException {
432 // Check every metadata key looking for a characteristics we can create
433 var config = taggedItem.getConfiguration();
434 if (config == null) {
437 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
438 .collect(Collectors.toList())) {
439 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(entry.getKey(),
441 if (characteristic.isPresent()) {
442 accessory.addCharacteristic(characteristic.get());
448 * creates HomeKit services for an openhab item that are members of this group item.
450 * @param taggedItem openhab item tagged as HomeKit item
451 * @param AbstractHomekitAccessoryImpl the accessory to add services to
452 * @param metadataRegistry openhab metadata registry required to get item meta information
453 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
455 * @param settings OH settings
456 * @param ancestorServices set of all accessories/services under the same root accessory, for
457 * for preventing circular references
458 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
461 private static void addLinkedServices(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
462 MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings,
463 Set<HomekitTaggedItem> ancestorServices) throws HomekitException {
464 final var item = taggedItem.getItem();
465 if (!(item instanceof GroupItem)) {
469 for (var groupMember : ((GroupItem) item).getMembers().stream()
470 .sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
471 final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
472 var accessoryTypes = characteristicTypes.stream().filter(HomekitAccessoryFactory::isRootAccessory)
473 .collect(Collectors.toList());
475 LOGGER.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
476 if (accessoryTypes.isEmpty()) {
480 if (accessoryTypes.size() > 1) {
481 LOGGER.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
482 groupMember.getName());
486 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
488 final var accessoryType = accessoryTypes.iterator().next().getKey();
489 LOGGER.trace("Item {} is a HomeKit sub-accessory of type {}.", groupMember.getName(), accessoryType);
490 final var itemProxy = new HomekitOHItemProxy(groupMember);
491 final var subTaggedItem = new HomekitTaggedItem(itemProxy, accessoryType, itemConfiguration);
492 final var subAccessory = create(subTaggedItem, metadataRegistry, updater, settings, ancestorServices);
493 subAccessory.promoteNameCharacteristic();
495 if (subAccessory.isLinkable(accessory)) {
496 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
498 accessory.getServices().add(subAccessory.getPrimaryService());
504 * collect optional HomeKit characteristics for a OH item.
506 * @param taggedItem main OH item
507 * @param metadataRegistry OH metadata registry
508 * @return a map with characteristics and corresponding OH items
510 private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
511 MetadataRegistry metadataRegistry) {
512 Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
513 if (taggedItem.isGroup()) {
514 GroupItem groupItem = (GroupItem) taggedItem.getItem();
515 groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
516 .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
517 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
518 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
520 getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
521 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
522 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
523 (GenericItem) taggedItem.getItem()));
525 LOGGER.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
526 return Collections.unmodifiableMap(characteristicItems);
530 * return true is characteristic is a mandatory characteristic for the accessory.
533 * @param characteristic characteristic
534 * @return true if characteristic is mandatory, false if not mandatory
536 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
537 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
538 && getRequiredCharacteristics(item).contains(characteristic);
542 * check whether accessory is root accessory, i.e. without characteristic tag.
544 * @param accessory accessory
545 * @return true if accessory has not characteristic.
547 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
548 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
552 * check whether characteristic belongs to the specific accessory type.
553 * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
555 * @param accessoryType accessory type
556 * @param characteristic characteristic
557 * @return true if characteristic belongs to the accessory type.
559 private static boolean belongsToType(HomekitAccessoryType accessoryType,
560 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
561 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));