]> git.basschouten.com Git - openhab-addons.git/blob
d31ee127bfe10fb832553857e1dd9f8e19f575a5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 java.lang.reflect.InvocationTargetException;
16 import java.math.BigDecimal;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Map.Entry;
23 import java.util.Optional;
24 import java.util.concurrent.CompletableFuture;
25 import java.util.concurrent.ExecutionException;
26
27 import javax.json.Json;
28 import javax.json.JsonObjectBuilder;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.core.items.GenericItem;
33 import org.openhab.core.items.Item;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.OpenClosedType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.types.State;
38 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
39 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
40 import org.openhab.io.homekit.internal.HomekitSettings;
41 import org.openhab.io.homekit.internal.HomekitTaggedItem;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import io.github.hapjava.accessories.HomekitAccessory;
46 import io.github.hapjava.characteristics.Characteristic;
47 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
48 import io.github.hapjava.characteristics.impl.base.BaseCharacteristic;
49 import io.github.hapjava.services.Service;
50
51 /**
52  * Abstract class for Homekit Accessory implementations, this provides the
53  * accessory metadata using information from the underlying Item.
54  *
55  * @author Andy Lintner - Initial contribution
56  */
57 public abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
58     private final Logger logger = LoggerFactory.getLogger(AbstractHomekitAccessoryImpl.class);
59     private final List<HomekitTaggedItem> characteristics;
60     private final HomekitTaggedItem accessory;
61     private final HomekitAccessoryUpdater updater;
62     private final HomekitSettings settings;
63     private final List<Service> services;
64     private final Map<Class<? extends Characteristic>, Characteristic> rawCharacteristics;
65
66     public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List<HomekitTaggedItem> characteristics,
67             HomekitAccessoryUpdater updater, HomekitSettings settings) {
68         this.characteristics = characteristics;
69         this.accessory = accessory;
70         this.updater = updater;
71         this.services = new ArrayList<>();
72         this.settings = settings;
73         this.rawCharacteristics = new HashMap<>();
74     }
75
76     /**
77      * @param parentAccessory The primary service to link to.
78      * @return If this accessory should be nested as a linked service below a primary service,
79      *         rather than as a sibling.
80      */
81     public boolean isLinkable(HomekitAccessory parentAccessory) {
82         return false;
83     }
84
85     /**
86      * @return If this accessory is only valid as a linked service, not as a standalone accessory.
87      */
88     public boolean isLinkedServiceOnly() {
89         return false;
90     }
91
92     @NonNullByDefault
93     public Optional<HomekitTaggedItem> getCharacteristic(HomekitCharacteristicType type) {
94         return characteristics.stream().filter(c -> c.getCharacteristicType() == type).findAny();
95     }
96
97     @Override
98     public int getId() {
99         return accessory.getId();
100     }
101
102     @Override
103     public CompletableFuture<String> getName() {
104         return CompletableFuture.completedFuture(accessory.getItem().getLabel());
105     }
106
107     @Override
108     public CompletableFuture<String> getManufacturer() {
109         return CompletableFuture.completedFuture("none");
110     }
111
112     @Override
113     public CompletableFuture<String> getModel() {
114         return CompletableFuture.completedFuture("none");
115     }
116
117     @Override
118     public CompletableFuture<String> getSerialNumber() {
119         return CompletableFuture.completedFuture(accessory.getItem().getName());
120     }
121
122     @Override
123     public CompletableFuture<String> getFirmwareRevision() {
124         return CompletableFuture.completedFuture("none");
125     }
126
127     @Override
128     public void identify() {
129         // We're not going to support this for now
130     }
131
132     public HomekitTaggedItem getRootAccessory() {
133         return accessory;
134     }
135
136     @Override
137     public Collection<Service> getServices() {
138         return this.services;
139     }
140
141     protected HomekitAccessoryUpdater getUpdater() {
142         return updater;
143     }
144
145     protected HomekitSettings getSettings() {
146         return settings;
147     }
148
149     @NonNullByDefault
150     protected void subscribe(HomekitCharacteristicType characteristicType,
151             HomekitCharacteristicChangeCallback callback) {
152         final Optional<HomekitTaggedItem> characteristic = getCharacteristic(characteristicType);
153         if (characteristic.isPresent()) {
154             getUpdater().subscribe((GenericItem) characteristic.get().getItem(), characteristicType.getTag(), callback);
155         } else {
156             logger.warn("Missing mandatory characteristic {}", characteristicType);
157         }
158     }
159
160     @NonNullByDefault
161     protected void unsubscribe(HomekitCharacteristicType characteristicType) {
162         final Optional<HomekitTaggedItem> characteristic = getCharacteristic(characteristicType);
163         if (characteristic.isPresent()) {
164             getUpdater().unsubscribe((GenericItem) characteristic.get().getItem(), characteristicType.getTag());
165         } else {
166             logger.warn("Missing mandatory characteristic {}", characteristicType);
167         }
168     }
169
170     protected @Nullable State getState(HomekitCharacteristicType characteristic) {
171         final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
172         if (taggedItem.isPresent()) {
173             return taggedItem.get().getItem().getState();
174         }
175         logger.debug("State for characteristic {} at accessory {} cannot be retrieved.", characteristic,
176                 accessory.getName());
177         return null;
178     }
179
180     protected @Nullable <T extends State> T getStateAs(HomekitCharacteristicType characteristic, Class<T> type) {
181         final State state = getState(characteristic);
182         if (state != null) {
183             return state.as(type);
184         }
185         return null;
186     }
187
188     protected @Nullable Double getStateAsTemperature(HomekitCharacteristicType characteristic) {
189         return HomekitCharacteristicFactory.stateAsTemperature(getState(characteristic));
190     }
191
192     @NonNullByDefault
193     protected <T extends Item> Optional<T> getItem(HomekitCharacteristicType characteristic, Class<T> type) {
194         final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
195         if (taggedItem.isPresent()) {
196             final Item item = taggedItem.get().getItem();
197             if (type.isInstance(item)) {
198                 return Optional.of((T) item);
199             } else {
200                 logger.warn("Unsupported item type for characteristic {} at accessory {}. Expected {}, got {}",
201                         characteristic, accessory.getItem().getName(), type, taggedItem.get().getItem().getClass());
202             }
203         } else {
204             logger.warn("Mandatory characteristic {} not found at accessory {}. ", characteristic,
205                     accessory.getItem().getName());
206
207         }
208         return Optional.empty();
209     }
210
211     /**
212      * return configuration attached to the root accessory, e.g. groupItem.
213      * Note: result will be casted to the type of the default value.
214      * The type for number is BigDecimal.
215      *
216      * @param key configuration key
217      * @param defaultValue default value
218      * @param <T> expected type
219      * @return configuration value
220      */
221     @NonNullByDefault
222     protected <T> T getAccessoryConfiguration(String key, T defaultValue) {
223         return accessory.getConfiguration(key, defaultValue);
224     }
225
226     /**
227      * return configuration attached to the root accessory, e.g. groupItem.
228      *
229      * @param key configuration key
230      * @param defaultValue default value
231      * @return configuration value
232      */
233     @NonNullByDefault
234     protected boolean getAccessoryConfigurationAsBoolean(String key, boolean defaultValue) {
235         return accessory.getConfigurationAsBoolean(key, defaultValue);
236     }
237
238     /**
239      * return configuration of the characteristic item, e.g. currentTemperature.
240      * Note: result will be casted to the type of the default value.
241      * The type for number is BigDecimal.
242      *
243      * @param characteristicType characteristic type
244      * @param key configuration key
245      * @param defaultValue default value
246      * @param <T> expected type
247      * @return configuration value
248      */
249     @NonNullByDefault
250     protected <T> T getAccessoryConfiguration(HomekitCharacteristicType characteristicType, String key,
251             T defaultValue) {
252         return getCharacteristic(characteristicType)
253                 .map(homekitTaggedItem -> homekitTaggedItem.getConfiguration(key, defaultValue)).orElse(defaultValue);
254     }
255
256     /**
257      * update mapping with values from item configuration.
258      * it checks for all keys from the mapping whether there is configuration at item with the same key and if yes,
259      * replace the value.
260      *
261      * @param characteristicType characteristicType to identify item
262      * @param map mapping to update
263      * @param customEnumList list to store custom state enumeration
264      */
265     @NonNullByDefault
266     protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map,
267             @Nullable List<T> customEnumList) {
268         getCharacteristic(characteristicType).ifPresent(c -> {
269             final Map<String, Object> configuration = c.getConfiguration();
270             if (configuration != null) {
271                 map.forEach((k, current_value) -> {
272                     final Object new_value = configuration.get(k.toString());
273                     if (new_value instanceof String) {
274                         map.put(k, (String) new_value);
275                         if (customEnumList != null) {
276                             customEnumList.add(k);
277                         }
278                     }
279                 });
280             }
281         });
282     }
283
284     @NonNullByDefault
285     protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map) {
286         updateMapping(characteristicType, map, null);
287     }
288
289     /**
290      * takes item state as value and retrieves the key for that value from mapping.
291      * e.g. used to map StringItem value to HomeKit Enum
292      *
293      * @param characteristicType characteristicType to identify item
294      * @param mapping mapping
295      * @param defaultValue default value if nothing found in mapping
296      * @param <T> type of the result derived from
297      * @return key for the value
298      */
299     @NonNullByDefault
300     protected <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map<T, String> mapping,
301             T defaultValue) {
302         final Optional<HomekitTaggedItem> c = getCharacteristic(characteristicType);
303         if (c.isPresent()) {
304             final State state = c.get().getItem().getState();
305             logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", characteristicType.getTag(),
306                     state, mapping);
307             if (state instanceof StringType) {
308                 return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue()))
309                         .findAny().map(Entry::getKey).orElseGet(() -> {
310                             logger.warn(
311                                     "Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.",
312                                     state.toString(), characteristicType.getTag(), c.get().getName(), mapping.values(),
313                                     defaultValue);
314                             return defaultValue;
315                         });
316             }
317         }
318         return defaultValue;
319     }
320
321     @NonNullByDefault
322     protected void addCharacteristic(HomekitTaggedItem item, Characteristic characteristic)
323             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
324         characteristics.add(item);
325         addCharacteristic(characteristic);
326     }
327
328     /**
329      * @param type
330      * @param characteristic
331      */
332     @NonNullByDefault
333     public void addCharacteristic(Characteristic characteristic)
334             throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
335         if (rawCharacteristics.containsKey(characteristic.getClass())) {
336             logger.warn("Accessory {} already has a characteristic of type {}; ignoring additional definition.",
337                     accessory.getName(), characteristic.getClass().getSimpleName());
338             return;
339         }
340         rawCharacteristics.put(characteristic.getClass(), characteristic);
341         var service = getPrimaryService();
342         // find the corresponding add method at service and call it.
343         service.getClass().getMethod("addOptionalCharacteristic", characteristic.getClass()).invoke(service,
344                 characteristic);
345     }
346
347     @NonNullByDefault
348     public <T> Optional<T> getCharacteristic(Class<? extends T> klazz) {
349         return Optional.ofNullable((T) rawCharacteristics.get(klazz));
350     }
351
352     /**
353      * create boolean reader with ON state mapped to trueOnOffValue or trueOpenClosedValue depending of item type
354      *
355      * @param characteristicType characteristic id
356      * @param trueOnOffValue ON value for switch
357      * @param trueOpenClosedValue ON value for contact
358      * @return boolean read
359      * @throws IncompleteAccessoryException
360      */
361     @NonNullByDefault
362     protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType,
363             OnOffType trueOnOffValue, OpenClosedType trueOpenClosedValue) throws IncompleteAccessoryException {
364         return new BooleanItemReader(
365                 getItem(characteristicType, GenericItem.class)
366                         .orElseThrow(() -> new IncompleteAccessoryException(characteristicType)),
367                 trueOnOffValue, trueOpenClosedValue);
368     }
369
370     /**
371      * create boolean reader for a number item with ON state mapped to the value of the
372      * item being above a given threshold
373      *
374      * @param characteristicType characteristic id
375      * @param trueThreshold threshold for true of number item
376      * @param invertThreshold result is true if item is less than threshold, instead of more
377      * @return boolean read
378      * @throws IncompleteAccessoryException
379      */
380     @NonNullByDefault
381     protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType,
382             BigDecimal trueThreshold, boolean invertThreshold) throws IncompleteAccessoryException {
383         final HomekitTaggedItem taggedItem = getCharacteristic(characteristicType)
384                 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType));
385         return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
386                 taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN, trueThreshold, invertThreshold);
387     }
388
389     /**
390      * create boolean reader with default ON/OFF mapping considering inverted flag
391      *
392      * @param characteristicType characteristic id
393      * @return boolean reader
394      * @throws IncompleteAccessoryException
395      */
396     @NonNullByDefault
397     protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType)
398             throws IncompleteAccessoryException {
399         final HomekitTaggedItem taggedItem = getCharacteristic(characteristicType)
400                 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType));
401         return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
402                 taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
403     }
404
405     /**
406      * Calculates a string as json of the configuration for this accessory, suitable for seeing
407      * if the structure has changed, and building a dummy accessory for it. It is _not_ suitable
408      * for actual publishing to by HAP-Java to iOS devices, since all the IIDs will be set to 0.
409      * The IIDs will get replaced by actual values by HAP-Java inside of DummyHomekitCharacteristic.
410      */
411     public String toJson() {
412         var builder = Json.createArrayBuilder();
413         getServices().forEach(s -> {
414             builder.add(serviceToJson(s));
415         });
416         return builder.build().toString();
417     }
418
419     private JsonObjectBuilder serviceToJson(Service service) {
420         var serviceBuilder = Json.createObjectBuilder();
421         serviceBuilder.add("type", service.getType());
422         var characteristics = Json.createArrayBuilder();
423
424         service.getCharacteristics().stream().sorted((l, r) -> l.getClass().getName().compareTo(r.getClass().getName()))
425                 .forEach(c -> {
426                     try {
427                         var cJson = c.toJson(0).get();
428                         var cBuilder = Json.createObjectBuilder();
429                         // Need to copy over everything except the current value, which we instead
430                         // reach in and get the default value
431                         cJson.forEach((k, v) -> {
432                             if (k.equals("value")) {
433                                 Object defaultValue = ((BaseCharacteristic) c).getDefault();
434                                 if (defaultValue instanceof Boolean) {
435                                     cBuilder.add("value", (boolean) defaultValue);
436                                 } else if (defaultValue instanceof Integer) {
437                                     cBuilder.add("value", (int) defaultValue);
438                                 } else if (defaultValue instanceof Double) {
439                                     cBuilder.add("value", (double) defaultValue);
440                                 } else {
441                                     cBuilder.add("value", defaultValue.toString());
442                                 }
443                             } else {
444                                 cBuilder.add(k, v);
445                             }
446                         });
447                         characteristics.add(cBuilder.build());
448                     } catch (InterruptedException | ExecutionException e) {
449                     }
450                 });
451         serviceBuilder.add("c", characteristics);
452
453         if (!service.getLinkedServices().isEmpty()) {
454             var linkedServices = Json.createArrayBuilder();
455             service.getLinkedServices().forEach(s -> linkedServices.add(serviceToJson(s)));
456             serviceBuilder.add("ls", linkedServices);
457         }
458         return serviceBuilder;
459     }
460 }