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