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