2 * Copyright (c) 2010-2023 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;
55 import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
58 * Creates a HomekitAccessory for a given HomekitTaggedItem.
60 * @author Andy Lintner - Initial contribution
61 * @author Eugen Freiter - refactoring for optional characteristics
64 public class HomekitAccessoryFactory {
65 private static final Logger logger = LoggerFactory.getLogger(HomekitAccessoryFactory.class);
66 public static final String METADATA_KEY = "homekit"; // prefix for HomeKit meta information in items.xml
68 /** List of mandatory attributes for each accessory type. **/
69 private static final Map<HomekitAccessoryType, HomekitCharacteristicType[]> MANDATORY_CHARACTERISTICS = new HashMap<HomekitAccessoryType, HomekitCharacteristicType[]>() {
71 put(ACCESSORY_GROUP, new HomekitCharacteristicType[] {});
72 put(LEAK_SENSOR, new HomekitCharacteristicType[] { LEAK_DETECTED_STATE });
73 put(MOTION_SENSOR, new HomekitCharacteristicType[] { MOTION_DETECTED_STATE });
74 put(OCCUPANCY_SENSOR, new HomekitCharacteristicType[] { OCCUPANCY_DETECTED_STATE });
75 put(CONTACT_SENSOR, new HomekitCharacteristicType[] { CONTACT_SENSOR_STATE });
76 put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
77 put(HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
78 put(AIR_QUALITY_SENSOR, new HomekitCharacteristicType[] { AIR_QUALITY });
79 put(SWITCH, new HomekitCharacteristicType[] { ON_STATE });
80 put(CARBON_DIOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_DIOXIDE_DETECTED_STATE });
81 put(CARBON_MONOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_MONOXIDE_DETECTED_STATE });
82 put(WINDOW_COVERING, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE });
83 put(LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
84 put(BASIC_FAN, new HomekitCharacteristicType[] { ON_STATE });
85 put(FAN, new HomekitCharacteristicType[] { ACTIVE_STATUS });
86 put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
87 put(TEMPERATURE_SENSOR, new HomekitCharacteristicType[] { CURRENT_TEMPERATURE });
88 put(THERMOSTAT, new HomekitCharacteristicType[] { CURRENT_HEATING_COOLING_STATE,
89 TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE, TARGET_TEMPERATURE });
90 put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
91 put(VALVE, new HomekitCharacteristicType[] { ACTIVE_STATUS, INUSE_STATUS });
93 new HomekitCharacteristicType[] { SECURITY_SYSTEM_CURRENT_STATE, SECURITY_SYSTEM_TARGET_STATE });
94 put(OUTLET, new HomekitCharacteristicType[] { ON_STATE, INUSE_STATUS });
95 put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
96 put(SMART_SPEAKER, new HomekitCharacteristicType[] { CURRENT_MEDIA_STATE, TARGET_MEDIA_STATE });
97 put(GARAGE_DOOR_OPENER,
98 new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE, OBSTRUCTION_STATUS });
99 put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE,
100 TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE });
101 put(WINDOW, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
102 put(DOOR, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
103 put(BATTERY, new HomekitCharacteristicType[] { BATTERY_LEVEL, BATTERY_LOW_STATUS });
104 put(FILTER_MAINTENANCE, new HomekitCharacteristicType[] { FILTER_CHANGE_INDICATION });
105 put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
106 put(FAUCET, new HomekitCharacteristicType[] { ACTIVE_STATUS });
107 put(MICROPHONE, new HomekitCharacteristicType[] { MUTE });
108 put(TELEVISION, new HomekitCharacteristicType[] { ACTIVE });
109 put(INPUT_SOURCE, new HomekitCharacteristicType[] {});
110 put(TELEVISION_SPEAKER, new HomekitCharacteristicType[] { MUTE });
114 /** List of service implementation for each accessory type. **/
115 private static final Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>>() {
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);
156 private static List<HomekitCharacteristicType> getRequiredCharacteristics(HomekitTaggedItem taggedItem) {
157 final List<HomekitCharacteristicType> characteristics = new ArrayList<>();
158 if (MANDATORY_CHARACTERISTICS.containsKey(taggedItem.getAccessoryType())) {
159 characteristics.addAll(Arrays.asList(MANDATORY_CHARACTERISTICS.get(taggedItem.getAccessoryType())));
161 if (taggedItem.getAccessoryType() == BATTERY) {
162 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
164 characteristics.add(BATTERY_CHARGING_STATE);
167 return characteristics;
171 * creates HomeKit accessory for an openhab item.
173 * @param taggedItem openhab item tagged as HomeKit item
174 * @param metadataRegistry openhab metadata registry required to get item meta information
175 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
177 * @param settings OH settings
178 * @return HomeKit accessory
179 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
182 public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
183 HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
184 Set<HomekitTaggedItem> ancestorServices = new HashSet<>();
185 return create(taggedItem, metadataRegistry, updater, settings, ancestorServices);
188 @SuppressWarnings("null")
189 private static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
190 HomekitAccessoryUpdater updater, HomekitSettings settings, Set<HomekitTaggedItem> ancestorServices)
191 throws HomekitException {
192 final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
193 logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
194 final List<HomekitTaggedItem> foundCharacteristics = getMandatoryCharacteristicsFromItem(taggedItem,
196 final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
197 if (foundCharacteristics.size() < mandatoryCharacteristics.size()) {
198 logger.warn("Accessory of type {} must have following characteristics {}. Found only {}",
199 accessoryType.getTag(), mandatoryCharacteristics, foundCharacteristics);
200 throw new HomekitException("Missing mandatory characteristics");
202 AbstractHomekitAccessoryImpl accessoryImpl;
204 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
206 if (accessoryImplClass != null) {
207 if (ancestorServices.contains(taggedItem)) {
208 logger.warn("Item {} has already been created. Perhaps you have circular Homekit accessory groups?",
209 taggedItem.getName());
210 throw new HomekitException("Circular accessory references");
212 ancestorServices.add(taggedItem);
213 accessoryImpl = accessoryImplClass.getConstructor(HomekitTaggedItem.class, List.class,
214 HomekitAccessoryUpdater.class, HomekitSettings.class)
215 .newInstance(taggedItem, foundCharacteristics, updater, settings);
216 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
217 addOptionalMetadataCharacteristics(taggedItem, accessoryImpl);
218 accessoryImpl.init();
219 addLinkedServices(taggedItem, accessoryImpl, metadataRegistry, updater, settings, ancestorServices);
220 return accessoryImpl;
222 logger.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
223 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
225 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
226 | InvocationTargetException e) {
227 logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
228 throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
233 * return HomeKit accessory types for an OH item based on meta data
235 * @param item OH item
236 * @param metadataRegistry meta data registry
237 * @return list of HomeKit accessory types and characteristics.
239 public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
240 MetadataRegistry metadataRegistry) {
241 final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
242 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
243 if (metadata != null) {
244 String[] tags = metadata.getValue().split(",");
245 for (String tag : tags) {
246 final String[] meta = tag.split("\\.");
247 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
248 if (accessoryType.isPresent()) { // it accessory, check for characteristic
249 HomekitAccessoryType type = accessoryType.get();
250 if (meta.length > 1) {
251 // it has characteristic as well
252 accessories.add(new SimpleEntry<>(type,
253 HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY)));
254 } else {// it has no characteristic
255 accessories.add(new SimpleEntry<>(type, EMPTY));
257 } else { // it is no accessory, so, maybe it is a characteristic
258 HomekitCharacteristicType.valueOfTag(meta[0].trim())
259 .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
266 public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
267 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
268 return metadata != null ? metadata.getConfiguration() : null;
272 * return list of HomeKit relevant groups linked to an accessory
274 * @param item OH item
275 * @param itemRegistry item registry
276 * @param metadataRegistry metadata registry
277 * @return list of relevant group items
279 public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
280 MetadataRegistry metadataRegistry) {
281 return item.getGroupNames().stream().flatMap(name -> {
282 final @Nullable Item groupItem = itemRegistry.get(name);
283 if (groupItem instanceof GroupItem) {
284 return Stream.of((GroupItem) groupItem);
286 return Stream.empty();
288 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
292 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
295 * @param taggedItem HomeKit tagged item
296 * @param metadataRegistry meta data registry
297 * @return list of mandatory
299 private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
300 MetadataRegistry metadataRegistry) {
301 List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
302 if (taggedItem.isGroup()) {
303 for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
304 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
307 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
309 logger.trace("Mandatory characteristics: {}", collectedCharacteristics);
310 return collectedCharacteristics;
314 * add mandatory HomeKit items for a given main item to a list of characteristics.
315 * Main item is use only to determine, which characteristics are mandatory.
316 * The characteristics are added to item.
317 * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
320 * @param mainItem main item
321 * @param characteristics list of characteristics
322 * @param item current item
323 * @param metadataRegistry meta date registry
325 private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
326 Item item, MetadataRegistry metadataRegistry) {
327 // get list of mandatory characteristics
328 List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
329 if (mandatoryCharacteristics.isEmpty()) {
330 // no mandatory characteristics linked to accessory type of mainItem. we are done
333 // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
334 // if we are adding not to the main item (typical for groups), create new proxy item.
335 final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
336 : new HomekitOHItemProxy(item);
337 // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
338 for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
340 // if the item has only accessory tag, e.g. TemperatureSensor,
341 // then we will link all mandatory characteristic to this item,
342 // e.g. we will link CurrentTemperature in case of TemperatureSensor.
343 // Note that accessories that are members of other accessories do _not_
344 // count - we're already constructing another root accessory.
345 if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
346 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
347 accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
348 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
350 // item has characteristic tag on it, so, adding it as that characteristic.
352 final HomekitCharacteristicType characteristic = accessory.getValue();
354 // check whether it is a mandatory characteristic. optional will be added later by another method.
355 if (belongsToType(mainItem.getAccessoryType(), accessory)
356 && isMandatoryCharacteristic(mainItem, characteristic)) {
357 characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
358 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
359 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
366 * add optional characteristics for given accessory.
368 * @param taggedItem main item
369 * @param accessory accessory
370 * @param metadataRegistry metadata registry
372 private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
373 MetadataRegistry metadataRegistry) {
374 Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
375 accessory.getRootAccessory(), metadataRegistry);
376 HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
377 proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
378 // an accessory can have multiple optional characteristics. iterate over them.
379 characteristics.forEach((type, item) -> {
381 // check whether a proxyItem already exists, if not create one.
382 final HomekitOHItemProxy proxyItem = Objects
383 .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
384 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
385 accessory.getRootAccessory().getAccessoryType(), type,
386 accessory.getRootAccessory().getRootDeviceGroupItem(),
387 getItemConfiguration(item, metadataRegistry));
388 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
389 accessory.getUpdater());
390 accessory.addCharacteristic(optionalItem, characteristic);
391 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
392 logger.warn("Unsupported optional HomeKit characteristic: type {}, characteristic type {}",
393 accessory.getPrimaryService(), type.getTag());
399 * add optional characteristics for given accessory from metadata
401 * @param taggedItem main item
402 * @param accessory accessory
404 private static void addOptionalMetadataCharacteristics(HomekitTaggedItem taggedItem,
405 AbstractHomekitAccessoryImpl accessory)
406 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, HomekitException {
407 // Check every metadata key looking for a characteristics we can create
408 var config = taggedItem.getConfiguration();
409 if (config == null) {
412 for (var entry : config.entrySet().stream().sorted((lhs, rhs) -> lhs.getKey().compareTo(rhs.getKey()))
413 .collect(Collectors.toList())) {
414 var characteristic = HomekitMetadataCharacteristicFactory.createCharacteristic(entry.getKey(),
416 if (characteristic.isPresent())
417 accessory.addCharacteristic(characteristic.get());
422 * creates HomeKit services for an openhab item that are members of this group item.
424 * @param taggedItem openhab item tagged as HomeKit item
425 * @param AbstractHomekitAccessoryImpl the accessory to add services to
426 * @param metadataRegistry openhab metadata registry required to get item meta information
427 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
429 * @param settings OH settings
430 * @param ancestorServices set of all accessories/services under the same root accessory, for
431 * for preventing circular references
432 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
435 private static void addLinkedServices(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
436 MetadataRegistry metadataRegistry, HomekitAccessoryUpdater updater, HomekitSettings settings,
437 Set<HomekitTaggedItem> ancestorServices) throws HomekitException {
438 final var item = taggedItem.getItem();
439 if (!(item instanceof GroupItem)) {
443 for (var groupMember : ((GroupItem) item).getMembers().stream()
444 .sorted((lhs, rhs) -> lhs.getName().compareTo(rhs.getName())).collect(Collectors.toList())) {
445 final var characteristicTypes = getAccessoryTypes(groupMember, metadataRegistry);
446 var accessoryTypes = characteristicTypes.stream().filter(HomekitAccessoryFactory::isRootAccessory)
447 .collect(Collectors.toList());
449 logger.trace("accessory types for {} are {}", groupMember.getName(), accessoryTypes);
450 if (accessoryTypes.isEmpty()) {
454 if (accessoryTypes.size() > 1) {
455 logger.warn("Item {} is a HomeKit sub-accessory, but multiple accessory types are not allowed.",
456 groupMember.getName());
460 final @Nullable Map<String, Object> itemConfiguration = getItemConfiguration(groupMember, metadataRegistry);
462 final var accessoryType = accessoryTypes.iterator().next().getKey();
463 logger.trace("Item {} is a HomeKit sub-accessory of type {}.", groupMember.getName(), accessoryType);
464 final var itemProxy = new HomekitOHItemProxy(groupMember);
465 final var subTaggedItem = new HomekitTaggedItem(itemProxy, accessoryType, itemConfiguration);
466 final var subAccessory = create(subTaggedItem, metadataRegistry, updater, settings, ancestorServices);
469 subAccessory.addCharacteristic(new NameCharacteristic(() -> subAccessory.getName()));
470 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
471 // This should never happen; all services should support NameCharacteristic as an optional
473 // If HAP-Java defined a service that doesn't support addOptionalCharacteristic(NameCharacteristic),
474 // Then it's a bug there, and we're just going to ignore the exception here.
477 if (subAccessory.isLinkable(accessory)) {
478 accessory.getPrimaryService().addLinkedService(subAccessory.getPrimaryService());
480 accessory.getServices().add(subAccessory.getPrimaryService());
486 * collect optional HomeKit characteristics for a OH item.
488 * @param taggedItem main OH item
489 * @param metadataRegistry OH metadata registry
490 * @return a map with characteristics and corresponding OH items
492 private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
493 MetadataRegistry metadataRegistry) {
494 Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
495 if (taggedItem.isGroup()) {
496 GroupItem groupItem = (GroupItem) taggedItem.getItem();
497 groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
498 .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
499 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
500 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
502 getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
503 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
504 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
505 (GenericItem) taggedItem.getItem()));
507 logger.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
508 return Collections.unmodifiableMap(characteristicItems);
512 * return true is characteristic is a mandatory characteristic for the accessory.
515 * @param characteristic characteristic
516 * @return true if characteristic is mandatory, false if not mandatory
518 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
519 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
520 && getRequiredCharacteristics(item).contains(characteristic);
524 * check whether accessory is root accessory, i.e. without characteristic tag.
526 * @param accessory accessory
527 * @return true if accessory has not characteristic.
529 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
530 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
534 * check whether characteristic belongs to the specific accessory type.
535 * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
537 * @param accessoryType accessory type
538 * @param characteristic characteristic
539 * @return true if characteristic belongs to the accessory type.
541 private static boolean belongsToType(HomekitAccessoryType accessoryType,
542 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
543 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));