2 * Copyright (c) 2010-2022 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.List;
26 import java.util.Map.Entry;
27 import java.util.Objects;
28 import java.util.Optional;
29 import java.util.TreeMap;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.core.items.GenericItem;
36 import org.openhab.core.items.GroupItem;
37 import org.openhab.core.items.Item;
38 import org.openhab.core.items.ItemRegistry;
39 import org.openhab.core.items.Metadata;
40 import org.openhab.core.items.MetadataKey;
41 import org.openhab.core.items.MetadataRegistry;
42 import org.openhab.io.homekit.internal.HomekitAccessoryType;
43 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
44 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
45 import org.openhab.io.homekit.internal.HomekitException;
46 import org.openhab.io.homekit.internal.HomekitOHItemProxy;
47 import org.openhab.io.homekit.internal.HomekitSettings;
48 import org.openhab.io.homekit.internal.HomekitTaggedItem;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import io.github.hapjava.characteristics.Characteristic;
53 import io.github.hapjava.services.Service;
56 * Creates a HomekitAccessory for a given HomekitTaggedItem.
58 * @author Andy Lintner - Initial contribution
59 * @author Eugen Freiter - refactoring for optional characteristics
62 public class HomekitAccessoryFactory {
63 private static final Logger logger = LoggerFactory.getLogger(HomekitAccessoryFactory.class);
64 public final static String METADATA_KEY = "homekit"; // prefix for HomeKit meta information in items.xml
66 /** List of mandatory attributes for each accessory type. **/
67 private final static Map<HomekitAccessoryType, HomekitCharacteristicType[]> MANDATORY_CHARACTERISTICS = new HashMap<HomekitAccessoryType, HomekitCharacteristicType[]>() {
69 put(LEAK_SENSOR, new HomekitCharacteristicType[] { LEAK_DETECTED_STATE });
70 put(MOTION_SENSOR, new HomekitCharacteristicType[] { MOTION_DETECTED_STATE });
71 put(OCCUPANCY_SENSOR, new HomekitCharacteristicType[] { OCCUPANCY_DETECTED_STATE });
72 put(CONTACT_SENSOR, new HomekitCharacteristicType[] { CONTACT_SENSOR_STATE });
73 put(SMOKE_SENSOR, new HomekitCharacteristicType[] { SMOKE_DETECTED_STATE });
74 put(HUMIDITY_SENSOR, new HomekitCharacteristicType[] { RELATIVE_HUMIDITY });
75 put(AIR_QUALITY_SENSOR, new HomekitCharacteristicType[] { AIR_QUALITY });
76 put(SWITCH, new HomekitCharacteristicType[] { ON_STATE });
77 put(CARBON_DIOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_DIOXIDE_DETECTED_STATE });
78 put(CARBON_MONOXIDE_SENSOR, new HomekitCharacteristicType[] { CARBON_MONOXIDE_DETECTED_STATE });
79 put(WINDOW_COVERING, new HomekitCharacteristicType[] { TARGET_POSITION, CURRENT_POSITION, POSITION_STATE });
80 put(LIGHTBULB, new HomekitCharacteristicType[] { ON_STATE });
81 put(BASIC_FAN, new HomekitCharacteristicType[] { ON_STATE });
82 put(FAN, new HomekitCharacteristicType[] { ACTIVE_STATUS });
83 put(LIGHT_SENSOR, new HomekitCharacteristicType[] { LIGHT_LEVEL });
84 put(TEMPERATURE_SENSOR, new HomekitCharacteristicType[] { CURRENT_TEMPERATURE });
85 put(THERMOSTAT, new HomekitCharacteristicType[] { CURRENT_HEATING_COOLING_STATE,
86 TARGET_HEATING_COOLING_STATE, CURRENT_TEMPERATURE, TARGET_TEMPERATURE });
87 put(LOCK, new HomekitCharacteristicType[] { LOCK_CURRENT_STATE, LOCK_TARGET_STATE });
88 put(VALVE, new HomekitCharacteristicType[] { ACTIVE_STATUS, INUSE_STATUS });
90 new HomekitCharacteristicType[] { SECURITY_SYSTEM_CURRENT_STATE, SECURITY_SYSTEM_TARGET_STATE });
91 put(OUTLET, new HomekitCharacteristicType[] { ON_STATE, INUSE_STATUS });
92 put(SPEAKER, new HomekitCharacteristicType[] { MUTE });
93 put(SMART_SPEAKER, new HomekitCharacteristicType[] { CURRENT_MEDIA_STATE, TARGET_MEDIA_STATE });
94 put(GARAGE_DOOR_OPENER,
95 new HomekitCharacteristicType[] { CURRENT_DOOR_STATE, TARGET_DOOR_STATE, OBSTRUCTION_STATUS });
96 put(HEATER_COOLER, new HomekitCharacteristicType[] { ACTIVE_STATUS, CURRENT_HEATER_COOLER_STATE,
97 TARGET_HEATER_COOLER_STATE, CURRENT_TEMPERATURE });
98 put(WINDOW, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
99 put(DOOR, new HomekitCharacteristicType[] { CURRENT_POSITION, TARGET_POSITION, POSITION_STATE });
100 put(BATTERY, new HomekitCharacteristicType[] { BATTERY_LEVEL, BATTERY_LOW_STATUS });
101 put(FILTER_MAINTENANCE, new HomekitCharacteristicType[] { FILTER_CHANGE_INDICATION });
102 put(SLAT, new HomekitCharacteristicType[] { CURRENT_SLAT_STATE });
103 put(FAUCET, new HomekitCharacteristicType[] { ACTIVE_STATUS });
104 put(MICROPHONE, new HomekitCharacteristicType[] { MUTE });
108 /** List of service implementation for each accessory type. **/
109 private final static Map<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>> SERVICE_IMPL_MAP = new HashMap<HomekitAccessoryType, Class<? extends AbstractHomekitAccessoryImpl>>() {
111 put(LEAK_SENSOR, HomekitLeakSensorImpl.class);
112 put(MOTION_SENSOR, HomekitMotionSensorImpl.class);
113 put(OCCUPANCY_SENSOR, HomekitOccupancySensorImpl.class);
114 put(CONTACT_SENSOR, HomekitContactSensorImpl.class);
115 put(SMOKE_SENSOR, HomekitSmokeSensorImpl.class);
116 put(HUMIDITY_SENSOR, HomekitHumiditySensorImpl.class);
117 put(AIR_QUALITY_SENSOR, HomekitAirQualitySensorImpl.class);
118 put(SWITCH, HomekitSwitchImpl.class);
119 put(CARBON_DIOXIDE_SENSOR, HomekitCarbonDioxideSensorImpl.class);
120 put(CARBON_MONOXIDE_SENSOR, HomekitCarbonMonoxideSensorImpl.class);
121 put(WINDOW_COVERING, HomekitWindowCoveringImpl.class);
122 put(LIGHTBULB, HomekitLightbulbImpl.class);
123 put(BASIC_FAN, HomekitBasicFanImpl.class);
124 put(FAN, HomekitFanImpl.class);
125 put(LIGHT_SENSOR, HomekitLightSensorImpl.class);
126 put(TEMPERATURE_SENSOR, HomekitTemperatureSensorImpl.class);
127 put(THERMOSTAT, HomekitThermostatImpl.class);
128 put(LOCK, HomekitLockImpl.class);
129 put(VALVE, HomekitValveImpl.class);
130 put(SECURITY_SYSTEM, HomekitSecuritySystemImpl.class);
131 put(OUTLET, HomekitOutletImpl.class);
132 put(SPEAKER, HomekitSpeakerImpl.class);
133 put(SMART_SPEAKER, HomekitSmartSpeakerImpl.class);
134 put(GARAGE_DOOR_OPENER, HomekitGarageDoorOpenerImpl.class);
135 put(DOOR, HomekitDoorImpl.class);
136 put(WINDOW, HomekitWindowImpl.class);
137 put(HEATER_COOLER, HomekitHeaterCoolerImpl.class);
138 put(BATTERY, HomekitBatteryImpl.class);
139 put(FILTER_MAINTENANCE, HomekitFilterMaintenanceImpl.class);
140 put(SLAT, HomekitSlatImpl.class);
141 put(FAUCET, HomekitFaucetImpl.class);
142 put(MICROPHONE, HomekitMicrophoneImpl.class);
146 private static List<HomekitCharacteristicType> getRequiredCharacteristics(HomekitTaggedItem taggedItem) {
147 final List<HomekitCharacteristicType> characteristics = new ArrayList<>();
148 if (MANDATORY_CHARACTERISTICS.containsKey(taggedItem.getAccessoryType())) {
149 characteristics.addAll(Arrays.asList(MANDATORY_CHARACTERISTICS.get(taggedItem.getAccessoryType())));
151 if (taggedItem.getAccessoryType() == BATTERY) {
152 final boolean isChargeable = taggedItem.getConfigurationAsBoolean(HomekitBatteryImpl.BATTERY_TYPE, false);
154 characteristics.add(BATTERY_CHARGING_STATE);
157 return characteristics;
161 * creates HomeKit accessory for an openhab item.
163 * @param taggedItem openhab item tagged as HomeKit item
164 * @param metadataRegistry openhab metadata registry required to get item meta information
165 * @param updater OH HomeKit update class that ensure the status sync between OH item and corresponding HomeKit
167 * @param settings OH settings
168 * @return HomeKit accessory
169 * @throws HomekitException exception in case HomeKit accessory could not be created, e.g. due missing mandatory
172 @SuppressWarnings("null")
173 public static AbstractHomekitAccessoryImpl create(HomekitTaggedItem taggedItem, MetadataRegistry metadataRegistry,
174 HomekitAccessoryUpdater updater, HomekitSettings settings) throws HomekitException {
175 final HomekitAccessoryType accessoryType = taggedItem.getAccessoryType();
176 logger.trace("Constructing {} of accessory type {}", taggedItem.getName(), accessoryType.getTag());
177 final List<HomekitTaggedItem> foundCharacteristics = getMandatoryCharacteristicsFromItem(taggedItem,
179 final List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(taggedItem);
180 if (foundCharacteristics.size() < mandatoryCharacteristics.size()) {
181 logger.warn("Accessory of type {} must have following characteristics {}. Found only {}",
182 accessoryType.getTag(), mandatoryCharacteristics, foundCharacteristics);
183 throw new HomekitException("Missing mandatory characteristics");
185 AbstractHomekitAccessoryImpl accessoryImpl;
187 final @Nullable Class<? extends AbstractHomekitAccessoryImpl> accessoryImplClass = SERVICE_IMPL_MAP
189 if (accessoryImplClass != null) {
190 accessoryImpl = accessoryImplClass.getConstructor(HomekitTaggedItem.class, List.class,
191 HomekitAccessoryUpdater.class, HomekitSettings.class)
192 .newInstance(taggedItem, foundCharacteristics, updater, settings);
193 addOptionalCharacteristics(taggedItem, accessoryImpl, metadataRegistry);
194 return accessoryImpl;
196 logger.warn("Unsupported HomeKit type: {}", accessoryType.getTag());
197 throw new HomekitException("Unsupported HomeKit type: " + accessoryType);
199 } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
200 | InvocationTargetException e) {
201 logger.warn("Cannot instantiate accessory implementation for accessory {}", accessoryType.getTag(), e);
202 throw new HomekitException("Cannot instantiate accessory implementation for accessory " + accessoryType);
207 * return HomeKit accessory types for an OH item based on meta data
209 * @param item OH item
210 * @param metadataRegistry meta data registry
211 * @return list of HomeKit accessory types and characteristics.
213 public static List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> getAccessoryTypes(Item item,
214 MetadataRegistry metadataRegistry) {
215 final List<Entry<HomekitAccessoryType, HomekitCharacteristicType>> accessories = new ArrayList<>();
216 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
217 if (metadata != null) {
218 String[] tags = metadata.getValue().split(",");
219 for (String tag : tags) {
220 final String[] meta = tag.split("\\.");
221 Optional<HomekitAccessoryType> accessoryType = HomekitAccessoryType.valueOfTag(meta[0].trim());
222 if (accessoryType.isPresent()) { // it accessory, check for characteristic
223 HomekitAccessoryType type = accessoryType.get();
224 if (meta.length > 1) {
225 // it has characteristic as well
226 accessories.add(new SimpleEntry<>(type,
227 HomekitCharacteristicType.valueOfTag(meta[1].trim()).orElse(EMPTY)));
228 } else {// it has no characteristic
229 accessories.add(new SimpleEntry<>(type, EMPTY));
231 } else { // it is no accessory, so, maybe it is a characteristic
232 HomekitCharacteristicType.valueOfTag(meta[0].trim())
233 .ifPresent(c -> accessories.add(new SimpleEntry<>(DUMMY, c)));
240 public static @Nullable Map<String, Object> getItemConfiguration(Item item, MetadataRegistry metadataRegistry) {
241 final @Nullable Metadata metadata = metadataRegistry.get(new MetadataKey(METADATA_KEY, item.getUID()));
242 return metadata != null ? metadata.getConfiguration() : null;
246 * return list of HomeKit relevant groups linked to an accessory
248 * @param item OH item
249 * @param itemRegistry item registry
250 * @param metadataRegistry metadata registry
251 * @return list of relevant group items
253 public static List<GroupItem> getAccessoryGroups(Item item, ItemRegistry itemRegistry,
254 MetadataRegistry metadataRegistry) {
255 return (item instanceof GroupItem) ? Collections.emptyList() : item.getGroupNames().stream().flatMap(name -> {
256 final @Nullable Item groupItem = itemRegistry.get(name);
257 if ((groupItem instanceof GroupItem) && ((GroupItem) groupItem).getBaseItem() == null) {
258 return Stream.of((GroupItem) groupItem);
260 return Stream.empty();
262 }).filter(groupItem -> !getAccessoryTypes(groupItem, metadataRegistry).isEmpty()).collect(Collectors.toList());
266 * collect all mandatory characteristics for a given tagged item, e.g. collect all mandatory HomeKit items from a
269 * @param taggedItem HomeKit tagged item
270 * @param metadataRegistry meta data registry
271 * @return list of mandatory
273 private static List<HomekitTaggedItem> getMandatoryCharacteristicsFromItem(HomekitTaggedItem taggedItem,
274 MetadataRegistry metadataRegistry) {
275 List<HomekitTaggedItem> collectedCharacteristics = new ArrayList<>();
276 if (taggedItem.isGroup()) {
277 for (Item item : ((GroupItem) taggedItem.getItem()).getMembers()) {
278 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, item, metadataRegistry);
281 addMandatoryCharacteristics(taggedItem, collectedCharacteristics, taggedItem.getItem(), metadataRegistry);
283 logger.trace("Mandatory characteristics: {}", collectedCharacteristics);
284 return collectedCharacteristics;
288 * add mandatory HomeKit items for a given main item to a list of characteristics.
289 * Main item is use only to determine, which characteristics are mandatory.
290 * The characteristics are added to item.
291 * e.g. mainItem could be a group tagged as "thermostat" and item could be item linked to the group and marked as
294 * @param mainItem main item
295 * @param characteristics list of characteristics
296 * @param item current item
297 * @param metadataRegistry meta date registry
299 private static void addMandatoryCharacteristics(HomekitTaggedItem mainItem, List<HomekitTaggedItem> characteristics,
300 Item item, MetadataRegistry metadataRegistry) {
301 // get list of mandatory characteristics
302 List<HomekitCharacteristicType> mandatoryCharacteristics = getRequiredCharacteristics(mainItem);
303 if (mandatoryCharacteristics.isEmpty()) {
304 // no mandatory characteristics linked to accessory type of mainItem. we are done
307 // check whether we are adding characteristic to the main item, and if yes, use existing item proxy.
308 // if we are adding not to the main item (typical for groups), create new proxy item.
309 final HomekitOHItemProxy itemProxy = mainItem.getItem().equals(item) ? mainItem.getProxyItem()
310 : new HomekitOHItemProxy(item);
311 // an item can have several tags, e.g. "ActiveStatus, InUse". we iterate here over all his tags
312 for (Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory : getAccessoryTypes(item,
314 // if the item has only accessory tag, e.g. TemperatureSensor,
315 // then we will link all mandatory characteristic to this item,
316 // e.g. we will link CurrentTemperature in case of TemperatureSensor.
317 // Note that accessories that are members of other accessories do _not_
318 // count - we're already constructing another root accessory.
319 if (isRootAccessory(accessory) && mainItem.getItem().equals(item)) {
320 mandatoryCharacteristics.forEach(c -> characteristics.add(new HomekitTaggedItem(itemProxy,
321 accessory.getKey(), c, mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
322 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry))));
324 // item has characteristic tag on it, so, adding it as that characteristic.
326 final HomekitCharacteristicType characteristic = accessory.getValue();
328 // check whether it is a mandatory characteristic. optional will be added later by another method.
329 if (belongsToType(mainItem.getAccessoryType(), accessory)
330 && isMandatoryCharacteristic(mainItem, characteristic)) {
331 characteristics.add(new HomekitTaggedItem(itemProxy, accessory.getKey(), characteristic,
332 mainItem.isGroup() ? (GroupItem) mainItem.getItem() : null,
333 HomekitAccessoryFactory.getItemConfiguration(item, metadataRegistry)));
340 * add optional characteristics for given accessory.
342 * @param taggedItem main item
343 * @param accessory accessory
344 * @param metadataRegistry metadata registry
346 private static void addOptionalCharacteristics(HomekitTaggedItem taggedItem, AbstractHomekitAccessoryImpl accessory,
347 MetadataRegistry metadataRegistry) {
348 Map<HomekitCharacteristicType, GenericItem> characteristics = getOptionalCharacteristics(
349 accessory.getRootAccessory(), metadataRegistry);
350 Service service = accessory.getPrimaryService();
351 HashMap<String, HomekitOHItemProxy> proxyItems = new HashMap<>();
352 proxyItems.put(taggedItem.getItem().getUID(), taggedItem.getProxyItem());
353 // an accessory can have multiple optional characteristics. iterate over them.
354 characteristics.forEach((type, item) -> {
356 // check whether a proxyItem already exists, if not create one.
357 final HomekitOHItemProxy proxyItem = Objects
358 .requireNonNull(proxyItems.computeIfAbsent(item.getUID(), k -> new HomekitOHItemProxy(item)));
359 final HomekitTaggedItem optionalItem = new HomekitTaggedItem(proxyItem,
360 accessory.getRootAccessory().getAccessoryType(), type,
361 accessory.getRootAccessory().getRootDeviceGroupItem(),
362 getItemConfiguration(item, metadataRegistry));
363 final Characteristic characteristic = HomekitCharacteristicFactory.createCharacteristic(optionalItem,
364 accessory.getUpdater());
365 // find the corresponding add method at service and call it.
366 service.getClass().getMethod("addOptionalCharacteristic", characteristic.getClass()).invoke(service,
368 accessory.addCharacteristic(optionalItem);
369 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | HomekitException e) {
370 logger.warn("Unsupported optional HomeKit characteristic: service type {}, characteristic type {}",
371 service.getType(), type.getTag());
377 * collect optional HomeKit characteristics for an OH item.
379 * @param taggedItem main OH item
380 * @param metadataRegistry OH metadata registry
381 * @return a map with characteristics and corresponding OH items
383 private static Map<HomekitCharacteristicType, GenericItem> getOptionalCharacteristics(HomekitTaggedItem taggedItem,
384 MetadataRegistry metadataRegistry) {
385 Map<HomekitCharacteristicType, GenericItem> characteristicItems = new TreeMap<>();
386 if (taggedItem.isGroup()) {
387 GroupItem groupItem = (GroupItem) taggedItem.getItem();
388 groupItem.getMembers().forEach(item -> getAccessoryTypes(item, metadataRegistry).stream()
389 .filter(c -> !isRootAccessory(c)).filter(c -> belongsToType(taggedItem.getAccessoryType(), c))
390 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
391 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(), (GenericItem) item)));
393 getAccessoryTypes(taggedItem.getItem(), metadataRegistry).stream().filter(c -> !isRootAccessory(c))
394 .filter(c -> !isMandatoryCharacteristic(taggedItem, c.getValue()))
395 .forEach(characteristic -> characteristicItems.put(characteristic.getValue(),
396 (GenericItem) taggedItem.getItem()));
398 logger.trace("Optional characteristics for item {}: {}", taggedItem.getName(), characteristicItems.values());
399 return Collections.unmodifiableMap(characteristicItems);
403 * return true is characteristic is a mandatory characteristic for the accessory.
406 * @param characteristic characteristic
407 * @return true if characteristic is mandatory, false if not mandatory
409 private static boolean isMandatoryCharacteristic(HomekitTaggedItem item, HomekitCharacteristicType characteristic) {
410 return MANDATORY_CHARACTERISTICS.containsKey(item.getAccessoryType())
411 && getRequiredCharacteristics(item).contains(characteristic);
415 * check whether accessory is root accessory, i.e. without characteristic tag.
417 * @param accessory accessory
418 * @return true if accessory has not characteristic.
420 private static boolean isRootAccessory(Entry<HomekitAccessoryType, HomekitCharacteristicType> accessory) {
421 return ((accessory.getValue() == null) || (accessory.getValue() == EMPTY));
425 * check whether characteristic belongs to the specific accessory type.
426 * characteristic with no accessory type mentioned in metadata are considered as candidates for all types.
428 * @param accessoryType accessory type
429 * @param characteristic characteristic
430 * @return true if characteristic belongs to the accessory type.
432 private static boolean belongsToType(HomekitAccessoryType accessoryType,
433 Entry<HomekitAccessoryType, HomekitCharacteristicType> characteristic) {
434 return ((characteristic.getKey() == accessoryType) || (characteristic.getKey() == DUMMY));