2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.homekit.internal.accessories;
15 import java.math.BigDecimal;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.List;
20 import java.util.Map.Entry;
21 import java.util.Optional;
22 import java.util.concurrent.CompletableFuture;
23 import java.util.concurrent.ExecutionException;
25 import javax.json.Json;
26 import javax.json.JsonObjectBuilder;
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;
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;
49 * Abstract class for Homekit Accessory implementations, this provides the
50 * accessory metadata using information from the underlying Item.
52 * @author Andy Lintner - Initial contribution
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;
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;
72 protected Optional<HomekitTaggedItem> getCharacteristic(HomekitCharacteristicType type) {
73 return characteristics.stream().filter(c -> c.getCharacteristicType() == type).findAny();
78 return accessory.getId();
82 public CompletableFuture<String> getName() {
83 return CompletableFuture.completedFuture(accessory.getItem().getLabel());
87 public CompletableFuture<String> getManufacturer() {
88 return CompletableFuture.completedFuture("none");
92 public CompletableFuture<String> getModel() {
93 return CompletableFuture.completedFuture("none");
97 public CompletableFuture<String> getSerialNumber() {
98 return CompletableFuture.completedFuture(accessory.getItem().getName());
102 public CompletableFuture<String> getFirmwareRevision() {
103 return CompletableFuture.completedFuture("none");
107 public void identify() {
108 // We're not going to support this for now
111 public HomekitTaggedItem getRootAccessory() {
116 public Collection<Service> getServices() {
117 return this.services;
120 protected HomekitAccessoryUpdater getUpdater() {
124 protected HomekitSettings getSettings() {
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);
135 logger.warn("Missing mandatory characteristic {}", characteristicType);
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());
145 logger.warn("Missing mandatory characteristic {}", characteristicType);
149 protected @Nullable State getState(HomekitCharacteristicType characteristic) {
150 final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
151 if (taggedItem.isPresent()) {
152 return taggedItem.get().getItem().getState();
154 logger.debug("State for characteristic {} at accessory {} cannot be retrieved.", characteristic,
155 accessory.getName());
159 protected @Nullable <T extends State> T getStateAs(HomekitCharacteristicType characteristic, Class<T> type) {
160 final State state = getState(characteristic);
162 return state.as(type);
167 protected @Nullable Double getStateAsTemperature(HomekitCharacteristicType characteristic) {
168 return HomekitCharacteristicFactory.stateAsTemperature(getState(characteristic));
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);
179 logger.warn("Unsupported item type for characteristic {} at accessory {}. Expected {}, got {}",
180 characteristic, accessory.getItem().getName(), type, taggedItem.get().getItem().getClass());
183 logger.warn("Mandatory characteristic {} not found at accessory {}. ", characteristic,
184 accessory.getItem().getName());
187 return Optional.empty();
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.
195 * @param key configuration key
196 * @param defaultValue default value
197 * @param <T> expected type
198 * @return configuration value
201 protected <T> T getAccessoryConfiguration(String key, T defaultValue) {
202 return accessory.getConfiguration(key, defaultValue);
206 * return configuration attached to the root accessory, e.g. groupItem.
208 * @param key configuration key
209 * @param defaultValue default value
210 * @return configuration value
213 protected boolean getAccessoryConfigurationAsBoolean(String key, boolean defaultValue) {
214 return accessory.getConfigurationAsBoolean(key, defaultValue);
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.
222 * @param characteristicType characteristic type
223 * @param key configuration key
224 * @param defaultValue default value
225 * @param <T> expected type
226 * @return configuration value
229 protected <T> T getAccessoryConfiguration(HomekitCharacteristicType characteristicType, String key,
231 return getCharacteristic(characteristicType)
232 .map(homekitTaggedItem -> homekitTaggedItem.getConfiguration(key, defaultValue)).orElse(defaultValue);
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,
240 * @param characteristicType characteristicType to identify item
241 * @param map mapping to update
242 * @param customEnumList list to store custom state enumeration
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);
264 protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map) {
265 updateMapping(characteristicType, map, null);
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
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
279 protected <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map<T, String> mapping,
281 final Optional<HomekitTaggedItem> c = getCharacteristic(characteristicType);
283 final State state = c.get().getItem().getState();
284 logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", characteristicType.getTag(),
286 if (state instanceof StringType) {
287 return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue()))
288 .findAny().map(Entry::getKey).orElseGet(() -> {
290 "Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.",
291 state.toString(), characteristicType.getTag(), c.get().getName(), mapping.values(),
301 protected void addCharacteristic(HomekitTaggedItem characteristic) {
302 characteristics.add(characteristic);
306 * create boolean reader with ON state mapped to trueOnOffValue or trueOpenClosedValue depending of item type
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
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);
324 * create boolean reader for a number item with ON state mapped to the value of the
325 * item being above a given threshold
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
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);
343 * create boolean reader with default ON/OFF mapping considering inverted flag
345 * @param characteristicType characteristic id
346 * @return boolean reader
347 * @throws IncompleteAccessoryException
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);
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.
364 public String toJson() {
365 var builder = Json.createArrayBuilder();
366 getServices().forEach(s -> {
367 builder.add(serviceToJson(s));
369 return builder.build().toString();
372 private JsonObjectBuilder serviceToJson(Service service) {
373 var serviceBuilder = Json.createObjectBuilder();
374 serviceBuilder.add("type", service.getType());
375 var characteristics = Json.createArrayBuilder();
377 service.getCharacteristics().stream().sorted((l, r) -> l.getClass().getName().compareTo(r.getClass().getName()))
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);
394 cBuilder.add("value", defaultValue.toString());
400 characteristics.add(cBuilder.build());
401 } catch (InterruptedException | ExecutionException e) {
404 serviceBuilder.add("c", characteristics);
406 if (!service.getLinkedServices().isEmpty()) {
407 var linkedServices = Json.createArrayBuilder();
408 service.getLinkedServices().forEach(s -> linkedServices.add(serviceToJson(s)));
409 serviceBuilder.add("ls", linkedServices);
411 return serviceBuilder;