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