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