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