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> characteristics = new ArrayList<>();
196 final List<Characteristic> rawCharacteristics = new ArrayList<>();
198 getMandatoryCharacteristicsFromItem(taggedItem, metadataRegistry, characteristics, rawCharacteristics);
199 final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
200 if (characteristics.size() + rawCharacteristics.size() < mandatoryCharacteristics.size()) {
201 LOGGER.warn("Accessory of type {} must have following characteristics {}. Found only {}, {}",
202 accessoryType.getTag(), mandatoryCharacteristics, characteristics, rawCharacteristics);
203 throw new HomekitException("Missing mandatory characteristics");
205 AbstractHomekitAccessoryImpl accessoryImpl;
207 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
209 if (accessoryImplClass != null) {
210 if (ancestorServices.contains(taggedItem)) {
211 LOGGER.warn("Item {} has already been created. Perhaps you have circular Homekit accessory groups?",
212 taggedItem.getName());
213 throw new HomekitException("Circular accessory references");
215 accessoryImpl = accessoryImplClass
216 .getConstructor(HomekitTaggedItem.class, List.class, List.class, HomekitAccessoryUpdater.class,
217 HomekitSettings.class)
218 .newInstance(taggedItem, characteristics, rawCharacteristics, updater, settings);
219 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
220 addOptionalMetadataCharacteristics(taggedItem, accessoryImpl);
221 accessoryImpl.setIsLinkedService(!ancestorServices.isEmpty());
222 accessoryImpl.init();
223 ancestorServices.add(taggedItem);
224 addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
225 return accessoryImpl;
227 LOGGER.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
228 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
230 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
231 | InvocationTargetException e) {
232 LOGGER.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
233 throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
238 * return HomeKit accessory types for an OH item based on meta data
240 * @param item OH item
241 * @param metadataRegistry meta data registry
242 * @return list of HomeKit accessory types and characteristics.
244 public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
245 MetadataRegistry metadataRegistry) {
246 final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
247 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
248 if (metadata != null) {
249 String[] tags = metadata.getValue().split(",");
250 for (String tag : tags) {
251 final String[] meta = tag.split("\\.");
252 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
253 if (accessoryType.isPresent()) { // it accessory, check for characteristic
254 HomekitAccessoryType type = accessoryType.get();
255 if (meta.length > 1) {
256 // it has characteristic as well
257 accessories.add(new SimpleEntry<>(type, Objects
258 .requireNonNull(HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY))));
259 } else {// it has no characteristic
260 accessories.add(new SimpleEntry<>(type, EMPTY));
262 } else { // it is no accessory, so, maybe it is a characteristic
263 HomekitCharacteristicType.valueOfTag(meta[0].trim())
264 .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
271 public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
272 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
273 return metadata != null ? metadata.getConfiguration() : null;
277 * return list of HomeKit relevant groups linked to an accessory
279 * @param item OH item
280 * @param itemRegistry item registry
281 * @param metadataRegistry metadata registry
282 * @return list of relevant group items
284 public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
285 MetadataRegistry metadataRegistry) {
286 return item.getGroupNames().stream().flatMap(name -> {
287 final @Nullable Item itemFromRegistry = itemRegistry.get(name);
288 if (itemFromRegistry instanceof GroupItem groupItem) {
289 return Stream.of(groupItem);
291 return Stream.empty();
293 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
297 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
300 * @param taggedItem HomeKit tagged item
301 * @param metadataRegistry meta data registry
302 * @return list of mandatory
304 private static void getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
305 MetadataRegistry metadataRegistry, List<HomekitTaggedItem> characteristics,
306 List<Characteristic> rawCharacteristics) {
307 if (taggedItem.isGroup()) {
308 for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
309 addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, item, metadataRegistry);
312 addMandatoryCharacteristics(taggedItem, characteristics, rawCharacteristics, taggedItem.getItem(),
315 LOGGER.trace("Mandatory characteristics: {}, {}", characteristics, rawCharacteristics);
319 * add mandatory HomeKit items for a given main item to a list of characteristics.
320 * Main item is use only to determine, which characteristics are mandatory.
321 * The characteristics are added to item.
322 * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
325 * @param mainItem main item
326 * @param characteristics list of characteristics
327 * @param item current item
328 * @param metadataRegistry meta date registry
330 private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
331 List<Characteristic> rawCharacteristics, Item item, MetadataRegistry metadataRegistry) {
332 // get list of mandatory characteristics
333 List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
334 if (mandatoryCharacteristics.isEmpty()) {
335 // no mandatory characteristics linked to accessory type of mainItem. we are done
338 // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
339 // if we are adding not to the main item (typical for groups), create new proxy item.
340 final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
341 : new HomekitOHItemProxy(item);
342 // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
343 for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
345 // if the item has only accessory tag, e.g. TemperatureSensor,
346 // then we will link all mandatory characteristic to this item,
347 // e.g. we will link CurrentTemperature in case of TemperatureSensor.
348 // Note that accessories that are members of other accessories do _not_
349 // count - we're already constructing another root accessory.
350 if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
351 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
352 accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
353 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
355 // item has characteristic tag on it, so, adding it as that characteristic.
357 final HomekitCharacteristicType characteristic = accessory.getValue();
359 // check whether it is a mandatory characteristic. optional will be added later by another method.
360 if (belongsToType(mainItem.getAccessoryType(), accessory)
361 && isMandatoryCharacteristic(mainItem, characteristic)) {
362 characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
363 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
364 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
368 mandatoryCharacteristics.forEach(c -> {
369 // Check every metadata key looking for a characteristics we can create
370 var config = mainItem.getConfiguration();
371 if (config == null) {
374 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
375 .collect(Collectors.toList())) {
376 var type = HomekitCharacteristicType.valueOfTag(entry.getKey());
377 if (type.isPresent() && isMandatoryCharacteristic(mainItem, type.get())) {
378 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(type.get(),
381 characteristic.ifPresent(rc -> rawCharacteristics.add(rc));
388 * add optional characteristics for given accessory.
390 * @param taggedItem main item
391 * @param accessory accessory
392 * @param metadataRegistry metadata registry
394 private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
395 MetadataRegistry metadataRegistry) {
396 Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
397 accessory.getRootAccessory(), metadataRegistry);
398 HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
399 proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
400 // an accessory can have multiple optional characteristics. iterate over them.
401 characteristics.forEach((type, item) -> {
403 // check whether a proxyItem already exists, if not create one.
404 final HomekitOHItemProxy proxyItem = Objects
405 .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
406 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
407 accessory.getRootAccessory().getAccessoryType(), type,
408 accessory.getRootAccessory().getRootDeviceGroupItem(),
409 getItemConfiguration(item, metadataRegistry));
410 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
411 accessory.getUpdater());
412 accessory.addCharacteristic(optionalItem, characteristic);
413 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
414 LOGGER.warn("Unsupported optional HomeKit characteristic: type {}, characteristic type {}",
415 accessory.getPrimaryService(), type.getTag());
421 * add optional characteristics for given accessory from metadata
423 * @param taggedItem main item
424 * @param accessory accessory
426 private static void addOptionalMetadataCharacteristics(HomekitTaggedItem taggedItem,
427 AbstractHomekitAccessoryImpl accessory)
428 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, HomekitException {
429 // Check every metadata key looking for a characteristics we can create
430 var config = taggedItem.getConfiguration();
431 if (config == null) {
434 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
435 .collect(Collectors.toList())) {
436 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(entry.getKey(),
438 if (characteristic.isPresent()) {
439 accessory.addCharacteristic(characteristic.get());
445 * creates HomeKit services for an openhab item that are members of this group item.
447 * @param taggedItem openhab item tagged as HomeKit item
448 * @param AbstractHomekitAccessoryImpl the accessory to add services to
449 * @param metadataRegistry openhab metadata registry required to get item meta information
450 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
452 * @param settings OH settings
453 * @param ancestorServices set of all accessories/services under the same root accessory, for
454 * for preventing circular references
455 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
458 private static void addLinkedServices(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
459 MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings,
460 Set<HomekitTaggedItem> ancestorServices) throws HomekitException {
461 final var item = taggedItem.getItem();
462 if (!(item instanceof GroupItem)) {
466 for (var groupMember : ((GroupItem) item).getMembers().stream()
467 .sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
468 final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
469 var accessoryTypes = characteristicTypes.stream().filter(HomekitAccessoryFactory::isRootAccessory)
470 .collect(Collectors.toList());
472 LOGGER.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
473 if (accessoryTypes.isEmpty()) {
477 if (accessoryTypes.size() > 1) {
478 LOGGER.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
479 groupMember.getName());
483 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
485 final var accessoryType = accessoryTypes.iterator().next().getKey();
486 LOGGER.trace("Item {} is a HomeKit sub-accessory of type {}.", groupMember.getName(), accessoryType);
487 final var itemProxy = new HomekitOHItemProxy(groupMember);
488 final var subTaggedItem = new HomekitTaggedItem(itemProxy, accessoryType, itemConfiguration);
489 final var subAccessory = create(subTaggedItem, metadataRegistry, updater, settings, ancestorServices);
490 subAccessory.promoteNameCharacteristic();
492 if (subAccessory.isLinkable(accessory)) {
493 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
495 accessory.getServices().add(subAccessory.getPrimaryService());
501 * collect optional HomeKit characteristics for a OH item.
503 * @param taggedItem main OH item
504 * @param metadataRegistry OH metadata registry
505 * @return a map with characteristics and corresponding OH items
507 private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
508 MetadataRegistry metadataRegistry) {
509 Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
510 if (taggedItem.isGroup()) {
511 GroupItem groupItem = (GroupItem) taggedItem.getItem();
512 groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
513 .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
514 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
515 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
517 getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
518 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
519 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
520 (GenericItem) taggedItem.getItem()));
522 LOGGER.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
523 return Collections.unmodifiableMap(characteristicItems);
527 * return true is characteristic is a mandatory characteristic for the accessory.
530 * @param characteristic characteristic
531 * @return true if characteristic is mandatory, false if not mandatory
533 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
534 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
535 && getRequiredCharacteristics(item).contains(characteristic);
539 * check whether accessory is root accessory, i.e. without characteristic tag.
541 * @param accessory accessory
542 * @return true if accessory has not characteristic.
544 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
545 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
549 * check whether characteristic belongs to the specific accessory type.
550 * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
552 * @param accessoryType accessory type
553 * @param characteristic characteristic
554 * @return true if characteristic belongs to the accessory type.
556 private static boolean belongsToType(HomekitAccessoryType accessoryType,
557 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
558 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));