]> git.basschouten.com Git - openhab-addons.git/blob
2f45bbeddda8d820d2053702eca884407195f157
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.homekit.internal.accessories;
14
15 import static org.openhab.io.homekit.internal.HomekitAccessoryType.*;
16 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.*;
17
18 import java.lang.reflect.InvocationTargetException;
19 import java.util.*;
20 import java.util.AbstractMap.SimpleEntry;
21 import java.util.Map.Entry;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.core.items.GenericItem;
28 import org.openhab.core.items.GroupItem;
29 import org.openhab.core.items.Item;
30 import org.openhab.core.items.ItemRegistry;
31 import org.openhab.core.items.Metadata;
32 import org.openhab.core.items.MetadataKey;
33 import org.openhab.core.items.MetadataRegistry;
34 import org.openhab.core.library.items.ColorItem;
35 import org.openhab.core.library.items.DimmerItem;
36 import org.openhab.io.homekit.internal.HomekitAccessoryType;
37 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
38 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
39 import org.openhab.io.homekit.internal.HomekitException;
40 import org.openhab.io.homekit.internal.HomekitOHItemProxy;
41 import org.openhab.io.homekit.internal.HomekitSettings;
42 import org.openhab.io.homekit.internal.HomekitTaggedItem;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import io.github.hapjava.accessories.HomekitAccessory;
47 import io.github.hapjava.characteristics.Characteristic;
48 import io.github.hapjava.services.Service;
49
50 /**
51  * Creates a HomekitAccessory for a given HomekitTaggedItem.
52  *
53  * @author Andy Lintner - Initial contribution
54  * @author Eugen Freiter - refactoring for optional characteristics
55  */
56 @NonNullByDefault
57 @SuppressWarnings("deprecation")
58 public class HomekitAccessoryFactory {
59     private static final Logger logger = LoggerFactory.getLogger(HomekitAccessoryFactory.class);
60     public final static String METADATA_KEY = "homekit"; // prefix for HomeKit meta information in items.xml
61
62     /** List of mandatory attributes for each accessory type. **/
63     private final static Map<HomekitAccessoryType, HomekitCharacteristicType[]> MANDATORY_CHARACTERISTICS = new HashMap<HomekitAccessoryType, HomekitCharacteristicType[]>() {
64         {
65             put(LEAK_SENSOR, new HomekitCharacteristicType[] { LEAK_DETECTED_STATE });
66             put(MOTION_SENSOR, new HomekitCharacteristicType[] { MOTION_DETECTED_STATE });
67             put(OCCUPANCY_SENSOR, new HomekitCharacteristicType[] { OCCUPANCY_DETECTED_STATE });
68             put(CONTACT_SENSOR, new HomekitCharacteristicType[] { CONTACT_SENSOR_STATE });
69             put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
70             put(HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
71             put(AIR_QUALITY_SENSOR, new HomekitCharacteristicType[] { AIR_QUALITY });
72             put(SWITCH, new HomekitCharacteristicType[] { ON_STATE });
73             put(CARBON_DIOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_DIOXIDE_DETECTED_STATE });
74             put(CARBON_MONOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_MONOXIDE_DETECTED_STATE });
75             put(WINDOW_COVERING, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE });
76             put(LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
77             put(FAN, new HomekitCharacteristicType[] { ACTIVE_STATUS });
78             put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
79             put(TEMPERATURE_SENSOR, new HomekitCharacteristicType[] { CURRENT_TEMPERATURE });
80             put(THERMOSTAT, new HomekitCharacteristicType[] { CURRENT_HEATING_COOLING_STATE,
81                     TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE, TARGET_TEMPERATURE });
82             put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
83             put(VALVE, new HomekitCharacteristicType[] { ACTIVE_STATUS, INUSE_STATUS });
84             put(SECURITY_SYSTEM,
85                     new HomekitCharacteristicType[] { SECURITY_SYSTEM_CURRENT_STATE, SECURITY_SYSTEM_TARGET_STATE });
86             put(OUTLET, new HomekitCharacteristicType[] { ON_STATE, INUSE_STATUS });
87             put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
88             put(GARAGE_DOOR_OPENER,
89                     new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE, OBSTRUCTION_STATUS });
90             put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE,
91                     TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE });
92             // LEGACY
93             put(BLINDS, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE });
94             put(WINDOW, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
95             put(DOOR, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
96
97             put(OLD_HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
98             put(OLD_DIMMABLE_LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
99             put(OLD_COLORFUL_LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
100         }
101     };
102
103     /** List of service implementation for each accessory type. **/
104     private final static Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>>() {
105         {
106             put(LEAK_SENSOR, HomekitLeakSensorImpl.class);
107             put(MOTION_SENSOR, HomekitMotionSensorImpl.class);
108             put(OCCUPANCY_SENSOR, HomekitOccupancySensorImpl.class);
109             put(CONTACT_SENSOR, HomekitContactSensorImpl.class);
110             put(SMOKE_SENSOR, HomekitSmokeSensorImpl.class);
111             put(HUMIDITY_SENSOR, HomekitHumiditySensorImpl.class);
112             put(AIR_QUALITY_SENSOR, HomekitAirQualitySensorImpl.class);
113             put(SWITCH, HomekitSwitchImpl.class);
114             put(CARBON_DIOXIDE_SENSOR, HomekitCarbonDioxideSensorImpl.class);
115             put(CARBON_MONOXIDE_SENSOR, HomekitCarbonMonoxideSensorImpl.class);
116             put(WINDOW_COVERING, HomekitWindowCoveringImpl.class);
117             put(LIGHTBULB, HomekitLightbulbImpl.class);
118             put(FAN, HomekitFanImpl.class);
119             put(LIGHT_SENSOR, HomekitLightSensorImpl.class);
120             put(TEMPERATURE_SENSOR, HomekitTemperatureSensorImpl.class);
121             put(THERMOSTAT, HomekitThermostatImpl.class);
122             put(LOCK, HomekitLockImpl.class);
123             put(VALVE, HomekitValveImpl.class);
124             put(SECURITY_SYSTEM, HomekitSecuritySystemImpl.class);
125             put(OUTLET, HomekitOutletImpl.class);
126             put(SPEAKER, HomekitSpeakerImpl.class);
127             put(GARAGE_DOOR_OPENER, HomekitGarageDoorOpenerImpl.class);
128             put(BLINDS, HomekitWindowCoveringImpl.class);
129             put(DOOR, HomekitDoorImpl.class);
130             put(WINDOW, HomekitWindowImpl.class);
131             put(HEATER_COOLER, HomekitHeaterCoolerImpl.class);
132             put(OLD_HUMIDITY_SENSOR, HomekitHumiditySensorImpl.class);
133             put(OLD_DIMMABLE_LIGHTBULB, HomekitLightbulbImpl.class);
134             put(OLD_COLORFUL_LIGHTBULB, HomekitLightbulbImpl.class);
135         }
136     };
137
138     /** mapping of legacy attributes to new attributes. **/
139     private final static Map<HomekitCharacteristicType, HomekitCharacteristicType> LEGACY_CHARACTERISTICS_MAPPING = new HashMap<HomekitCharacteristicType, HomekitCharacteristicType>() {
140         {
141             put(OLD_CURRENT_HEATING_COOLING_STATE, CURRENT_HEATING_COOLING_STATE);
142             put(OLD_TARGET_HEATING_COOLING_MODE, TARGET_HEATING_COOLING_STATE);
143             put(OLD_TARGET_TEMPERATURE, TARGET_TEMPERATURE);
144             put(OLD_BATTERY_LOW_STATUS, BATTERY_LOW_STATUS);
145             put(VERY_OLD_TARGET_HEATING_COOLING_MODE, CURRENT_HEATING_COOLING_STATE);
146         }
147     };
148
149     /** list of optional implicit optional characteristics. mainly used for legacy accessory type */
150     private final static Map<HomekitAccessoryType, HomekitCharacteristicType[]> IMPLICIT_OPTIONAL_CHARACTERISTICS = new HashMap<HomekitAccessoryType, HomekitCharacteristicType[]>() {
151         {
152             put(OLD_DIMMABLE_LIGHTBULB, new HomekitCharacteristicType[] { BRIGHTNESS });
153             put(OLD_COLORFUL_LIGHTBULB, new HomekitCharacteristicType[] { HUE, SATURATION, BRIGHTNESS });
154
155         }
156     };
157
158     /**
159      * creates HomeKit accessory for a openhab item.
160      * 
161      * @param taggedItem openhab item tagged as HomeKit item
162      * @param metadataRegistry openhab metadata registry required to get item meta information
163      * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
164      *            characteristic.
165      * @param settings OH settings
166      * @return HomeKit accessory
167      * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
168      *             characteristic
169      */
170     @SuppressWarnings("null")
171     public static HomekitAccessory create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
172             HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
173         final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
174         logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
175         final List<HomekitTaggedItem> requiredCharacteristics = getMandatoryCharacteristics(taggedItem,
176                 metadataRegistry);
177         final HomekitCharacteristicType[] mandatoryCharacteristics = MANDATORY_CHARACTERISTICS.get(accessoryType);
178         if ((mandatoryCharacteristics != null) && (requiredCharacteristics.size() < mandatoryCharacteristics.length)) {
179             logger.warn("Accessory of type {} must have following characteristics {}. Found only {}",
180                     accessoryType.getTag(), mandatoryCharacteristics, requiredCharacteristics);
181             throw new HomekitException("Missing mandatory characteristics");
182         }
183         AbstractHomekitAccessoryImpl accessoryImpl;
184         try {
185             final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
186                     .get(accessoryType);
187             if (accessoryImplClass != null) {
188                 accessoryImpl = accessoryImplClass
189                         .getConstructor(HomekitTaggedItem.class, List.class, HomekitAccessoryUpdater.class,
190                                 HomekitSettings.class)
191                         .newInstance(taggedItem, requiredCharacteristics, updater, settings);
192                 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
193                 return accessoryImpl;
194             } else {
195                 logger.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
196                 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
197             }
198         } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
199                 | InvocationTargetException e) {
200             logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
201             throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
202         }
203     }
204
205     /**
206      * return HomeKit accessory types for a OH item based on meta data
207      * 
208      * @param item OH item
209      * @param metadataRegistry meta data registry
210      * @return list of HomeKit accessory types and characteristics.
211      */
212     public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
213             MetadataRegistry metadataRegistry) {
214         final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
215         final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
216         boolean legacyMode = metadata == null;
217         String[] tags = !legacyMode ? metadata.getValue().split(",") : item.getTags().toArray(new String[0]); // fallback
218         for (String tag : tags) {
219             final String[] meta = tag.split("\\.");
220             Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
221             if (accessoryType.isPresent()) { // it accessory, check for characteristic
222                 HomekitAccessoryType type = accessoryType.get();
223                 if ((legacyMode) && (type.equals(LIGHTBULB))) { // support old smart logic to convert Lighting to
224                                                                 // DimmableLighting or ColorfulLighting depending on
225                                                                 // item type
226                     if (item instanceof ColorItem) {
227                         type = OLD_COLORFUL_LIGHTBULB;
228                     } else if (item instanceof DimmerItem) {
229                         type = OLD_DIMMABLE_LIGHTBULB;
230                     }
231                 }
232                 if (meta.length > 1) {
233                     // it has characteristic as well
234                     accessories.add(new SimpleEntry<>(type,
235                             HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY)));
236                 } else {// it has no characteristic
237                     accessories.add(new SimpleEntry<>(type, EMPTY));
238                 }
239             } else { // it is no accessory, so, maybe it is a characteristic
240                 HomekitCharacteristicType.valueOfTag(meta[0].trim())
241                         .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
242             }
243         }
244         return accessories;
245     }
246
247     public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
248         final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
249         return metadata != null ? metadata.getConfiguration() : null;
250     }
251
252     /**
253      * return list of HomeKit relevant groups linked to an accessory
254      * 
255      * @param item OH item
256      * @param itemRegistry item registry
257      * @param metadataRegistry metadata registry
258      * @return list of relevant group items
259      */
260     public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
261             MetadataRegistry metadataRegistry) {
262         return item.getGroupNames().stream().flatMap(name -> {
263             final @Nullable Item groupItem = itemRegistry.get(name);
264             if ((groupItem instanceof GroupItem) && ((GroupItem) groupItem).getBaseItem() == null) {
265                 return Stream.of((GroupItem) groupItem);
266             } else {
267                 return Stream.empty();
268             }
269         }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
270     }
271
272     /**
273      * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
274      * GroupItem
275      * 
276      * @param taggedItem HomeKit tagged item
277      * @param metadataRegistry meta data registry
278      * @return list of mandatory
279      */
280     private static List<HomekitTaggedItem> getMandatoryCharacteristics(HomekitTaggedItem taggedItem,
281             MetadataRegistry metadataRegistry) {
282         List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
283         if (taggedItem.isGroup()) {
284             for (Item item : ((GroupItem) taggedItem.getItem()).getAllMembers()) {
285                 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
286             }
287         } else {
288             addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
289         }
290         logger.trace("Mandatory characteristics for item {} characteristics {}", taggedItem.getName(),
291                 collectedCharacteristics);
292         return collectedCharacteristics;
293     }
294
295     /**
296      * add mandatory HomeKit items for a given main item to a list of characteristics.
297      * Main item is use only to determine, which characteristics are mandatory.
298      * The characteristics are added to item.
299      * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
300      * TargetTemperature
301      *
302      * @param mainItem main item
303      * @param characteristics list of characteristics
304      * @param item current item
305      * @param metadataRegistry meta date registry
306      */
307     private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
308             Item item, MetadataRegistry metadataRegistry) {
309         // get list of mandatory characteristics
310         HomekitCharacteristicType[] mandatoryCharacteristics = MANDATORY_CHARACTERISTICS
311                 .get(mainItem.getAccessoryType());
312         if ((mandatoryCharacteristics == null) || (mandatoryCharacteristics.length == 0)) {
313             // no mandatory characteristics linked to accessory type of mainItem. we are done
314             return;
315         }
316         // check whether we adding characteristic to the main item, and if yes, use existing item proxy.
317         // if we adding no to the main item (typical for groups), create new proxy item.
318         final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
319                 : new HomekitOHItemProxy(item);
320         // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
321         for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
322                 metadataRegistry)) {
323             // if the item has only accessory tag, e.g. TemperatureSensor,
324             // the we will link all mandatory characteristic to this item,
325             // e.g. we will link CurrentTemperature in case of TemperatureSensor.
326             if (isRootAccessory(accessory)) {
327                 Arrays.stream(mandatoryCharacteristics)
328                         .forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), c,
329                                 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
330                                 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
331             } else {
332                 // item has characteristic tag on it, so, adding it as that characteristic.
333
334                 // check whether it is a legacy characteristic, i.e. old tag was used, and replaced by a new one
335                 final HomekitCharacteristicType characteristic = legacyCheck(accessory.getValue());
336
337                 // check whether it is a mandatory characteristic. optional will be added later by another method.
338                 if (isMandatoryCharacteristic(mainItem.getAccessoryType(), characteristic)) {
339                     characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
340                             mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
341                             HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
342                 }
343             }
344         }
345     }
346
347     /**
348      * add optional characteristic for given accessory.
349      *
350      * @param taggedItem main item
351      * @param accessory accessory
352      * @param metadataRegistry metadata registry
353      */
354     private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
355             MetadataRegistry metadataRegistry) {
356         Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
357                 accessory.getRootAccessory(), metadataRegistry);
358         Service service = accessory.getPrimaryService();
359         HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
360         proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
361         // an accessory can have multiple optional characteristics. iterate over them.
362         characteristics.forEach((type, item) -> {
363             try {
364                 // check whether a proxyItem already exists, if not create one.
365                 final HomekitOHItemProxy proxyItem = Objects
366                         .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
367                 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
368                         accessory.getRootAccessory().getAccessoryType(), type,
369                         accessory.getRootAccessory().getRootDeviceGroupItem(),
370                         getItemConfiguration(item, metadataRegistry));
371                 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
372                         accessory.getUpdater());
373                 // find the corresponding add method at service and call it.
374                 service.getClass().getMethod("addOptionalCharacteristic", characteristic.getClass()).invoke(service,
375                         characteristic);
376                 accessory.addCharacteristic(optionalItem);
377             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
378                 logger.warn("Unsupported optional HomeKit characteristic: service type {}, characteristic type {}",
379                         service.getType(), type.getTag());
380             }
381         });
382     }
383
384     /**
385      * collect optional HomeKit characteristics for a OH item.
386      * 
387      * @param taggedItem main OH item
388      * @param metadataRegistry OH metadata registry
389      * @return a map with characteristics and corresponding OH items
390      */
391     private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
392             MetadataRegistry metadataRegistry) {
393         Map<HomekitCharacteristicType, GenericItem> characteristicItems = new HashMap<>();
394         if (taggedItem.isGroup()) {
395             GroupItem groupItem = (GroupItem) taggedItem.getItem();
396             groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
397                     .filter(c -> !isRootAccessory(c))
398                     .filter(c -> !isMandatoryCharacteristic(taggedItem.getAccessoryType(), legacyCheck(c.getValue())))
399                     .forEach(characteristic -> characteristicItems.put(legacyCheck(characteristic.getValue()),
400                             (GenericItem) item)));
401         } else {
402             getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
403                     .filter(c -> !isMandatoryCharacteristic(taggedItem.getAccessoryType(), legacyCheck(c.getValue())))
404                     .forEach(characteristic -> characteristicItems.put(legacyCheck(characteristic.getValue()),
405                             (GenericItem) taggedItem.getItem()));
406             final HomekitCharacteristicType[] implicitOptionalCharacteristics = IMPLICIT_OPTIONAL_CHARACTERISTICS
407                     .get(taggedItem.getAccessoryType());
408             if (implicitOptionalCharacteristics != null) {
409                 Arrays.stream(implicitOptionalCharacteristics)
410                         .filter(c -> !isMandatoryCharacteristic(taggedItem.getAccessoryType(), c))
411                         .forEach(characteristic -> characteristicItems.put(legacyCheck(characteristic),
412                                 (GenericItem) taggedItem.getItem()));
413             }
414         }
415         logger.trace("Optional characteristics for item {} characteristics {}", taggedItem.getName(),
416                 characteristicItems);
417         return Collections.unmodifiableMap(characteristicItems);
418     }
419
420     /**
421      * return true is characteristic is a mandatory characteristic for the accessory.
422      * 
423      * @param accessory accessory
424      * @param characteristic characteristic
425      * @return true if characteristic is mandatory, false if not mandatory
426      */
427     private static boolean isMandatoryCharacteristic(HomekitAccessoryType accessory,
428             HomekitCharacteristicType characteristic) {
429         return MANDATORY_CHARACTERISTICS.containsKey(accessory)
430                 && Arrays.asList(MANDATORY_CHARACTERISTICS.get(accessory)).contains(characteristic);
431     }
432
433     /**
434      * check whether accessory is root accessory, i.e. without characteristic tag.
435      * 
436      * @param accessory accessory
437      * @return true if accessory has not characteristic.
438      */
439     private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
440         return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
441     }
442
443     /**
444      * check whether it is legacy characteristic and return new name in such case. otherwise return the input parameter
445      * unchanged.
446      * 
447      * @param characteristicType characteristic to check
448      * @return new characteristic type
449      */
450     private static HomekitCharacteristicType legacyCheck(HomekitCharacteristicType characteristicType) {
451         return LEGACY_CHARACTERISTICS_MAPPING.getOrDefault(characteristicType, characteristicType);
452     }
453 }