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;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.items.GenericItem;
27 import org.openhab.core.items.Item;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.OpenClosedType;
30 import org.openhab.core.library.types.StringType;
31 import org.openhab.core.types.State;
32 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
33 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
34 import org.openhab.io.homekit.internal.HomekitSettings;
35 import org.openhab.io.homekit.internal.HomekitTaggedItem;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import io.github.hapjava.accessories.HomekitAccessory;
40 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
41 import io.github.hapjava.services.Service;
44 * Abstract class for Homekit Accessory implementations, this provides the
45 * accessory metadata using information from the underlying Item.
47 * @author Andy Lintner - Initial contribution
49 abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
50 private final Logger logger = LoggerFactory.getLogger(AbstractHomekitAccessoryImpl.class);
51 private final List<HomekitTaggedItem> characteristics;
52 private final HomekitTaggedItem accessory;
53 private final HomekitAccessoryUpdater updater;
54 private final HomekitSettings settings;
55 private final List<Service> services;
57 public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List<HomekitTaggedItem> characteristics,
58 HomekitAccessoryUpdater updater, HomekitSettings settings) {
59 this.characteristics = characteristics;
60 this.accessory = accessory;
61 this.updater = updater;
62 this.services = new ArrayList<>();
63 this.settings = settings;
67 protected Optional<HomekitTaggedItem> getCharacteristic(HomekitCharacteristicType type) {
68 return characteristics.stream().filter(c -> c.getCharacteristicType() == type).findAny();
73 return accessory.getId();
77 public CompletableFuture<String> getName() {
78 return CompletableFuture.completedFuture(accessory.getItem().getLabel());
82 public CompletableFuture<String> getManufacturer() {
83 return CompletableFuture.completedFuture("none");
87 public CompletableFuture<String> getModel() {
88 return CompletableFuture.completedFuture("none");
92 public CompletableFuture<String> getSerialNumber() {
93 return CompletableFuture.completedFuture(accessory.getItem().getName());
97 public CompletableFuture<String> getFirmwareRevision() {
98 return CompletableFuture.completedFuture("none");
102 public void identify() {
103 // We're not going to support this for now
106 public HomekitTaggedItem getRootAccessory() {
111 public Collection<Service> getServices() {
112 return this.services;
115 protected HomekitAccessoryUpdater getUpdater() {
119 protected HomekitSettings getSettings() {
124 protected void subscribe(HomekitCharacteristicType characteristicType,
125 HomekitCharacteristicChangeCallback callback) {
126 final Optional<HomekitTaggedItem> characteristic = getCharacteristic(characteristicType);
127 if (characteristic.isPresent()) {
128 getUpdater().subscribe((GenericItem) characteristic.get().getItem(), characteristicType.getTag(), callback);
130 logger.warn("Missing mandatory characteristic {}", characteristicType);
135 protected void unsubscribe(HomekitCharacteristicType characteristicType) {
136 final Optional<HomekitTaggedItem> characteristic = getCharacteristic(characteristicType);
137 if (characteristic.isPresent()) {
138 getUpdater().unsubscribe((GenericItem) characteristic.get().getItem(), characteristicType.getTag());
140 logger.warn("Missing mandatory characteristic {}", characteristicType);
144 protected @Nullable State getState(HomekitCharacteristicType characteristic) {
145 final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
146 if (taggedItem.isPresent()) {
147 return taggedItem.get().getItem().getState();
149 logger.debug("State for characteristic {} at accessory {} cannot be retrieved.", characteristic,
150 accessory.getName());
154 protected @Nullable <T extends State> T getStateAs(HomekitCharacteristicType characteristic, Class<T> type) {
155 final State state = getState(characteristic);
157 return state.as(type);
162 protected @Nullable Double getStateAsTemperature(HomekitCharacteristicType characteristic) {
163 return HomekitCharacteristicFactory.stateAsTemperature(getState(characteristic));
167 protected <T extends Item> Optional<T> getItem(HomekitCharacteristicType characteristic, Class<T> type) {
168 final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
169 if (taggedItem.isPresent()) {
170 final Item item = taggedItem.get().getItem();
171 if (type.isInstance(item)) {
172 return Optional.of((T) item);
174 logger.warn("Unsupported item type for characteristic {} at accessory {}. Expected {}, got {}",
175 characteristic, accessory.getItem().getName(), type, taggedItem.get().getItem().getClass());
178 logger.warn("Mandatory characteristic {} not found at accessory {}. ", characteristic,
179 accessory.getItem().getName());
182 return Optional.empty();
186 * return configuration attached to the root accessory, e.g. groupItem.
187 * Note: result will be casted to the type of the default value.
188 * The type for number is BigDecimal.
190 * @param key configuration key
191 * @param defaultValue default value
192 * @param <T> expected type
193 * @return configuration value
196 protected <T> T getAccessoryConfiguration(String key, T defaultValue) {
197 return accessory.getConfiguration(key, defaultValue);
201 * return configuration attached to the root accessory, e.g. groupItem.
203 * @param key configuration key
204 * @param defaultValue default value
205 * @return configuration value
208 protected boolean getAccessoryConfigurationAsBoolean(String key, boolean defaultValue) {
209 return accessory.getConfigurationAsBoolean(key, defaultValue);
213 * return configuration of the characteristic item, e.g. currentTemperature.
214 * Note: result will be casted to the type of the default value.
215 * The type for number is BigDecimal.
217 * @param characteristicType characteristic type
218 * @param key configuration key
219 * @param defaultValue default value
220 * @param <T> expected type
221 * @return configuration value
224 protected <T> T getAccessoryConfiguration(HomekitCharacteristicType characteristicType, String key,
226 return getCharacteristic(characteristicType)
227 .map(homekitTaggedItem -> homekitTaggedItem.getConfiguration(key, defaultValue)).orElse(defaultValue);
231 * update mapping with values from item configuration.
232 * it checks for all keys from the mapping whether there is configuration at item with the same key and if yes,
235 * @param characteristicType characteristicType to identify item
236 * @param map mapping to update
237 * @param customEnumList list to store custom state enumeration
240 protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map,
241 @Nullable List<T> customEnumList) {
242 getCharacteristic(characteristicType).ifPresent(c -> {
243 final Map<String, Object> configuration = c.getConfiguration();
244 if (configuration != null) {
245 map.forEach((k, current_value) -> {
246 final Object new_value = configuration.get(k.toString());
247 if (new_value instanceof String) {
248 map.put(k, (String) new_value);
249 if (customEnumList != null) {
250 customEnumList.add(k);
259 protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map) {
260 updateMapping(characteristicType, map, null);
264 * takes item state as value and retrieves the key for that value from mapping.
265 * e.g. used to map StringItem value to HomeKit Enum
267 * @param characteristicType characteristicType to identify item
268 * @param mapping mapping
269 * @param defaultValue default value if nothing found in mapping
270 * @param <T> type of the result derived from
271 * @return key for the value
274 protected <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map<T, String> mapping,
276 final Optional<HomekitTaggedItem> c = getCharacteristic(characteristicType);
278 final State state = c.get().getItem().getState();
279 logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", characteristicType.getTag(),
281 if (state instanceof StringType) {
282 return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue()))
283 .findAny().map(Entry::getKey).orElseGet(() -> {
285 "Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.",
286 state.toString(), characteristicType.getTag(), c.get().getName(), mapping.values(),
296 protected void addCharacteristic(HomekitTaggedItem characteristic) {
297 characteristics.add(characteristic);
301 * create boolean reader with ON state mapped to trueOnOffValue or trueOpenClosedValue depending of item type
303 * @param characteristicType characteristic id
304 * @param trueOnOffValue ON value for switch
305 * @param trueOpenClosedValue ON value for contact
306 * @return boolean read
307 * @throws IncompleteAccessoryException
310 protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType,
311 OnOffType trueOnOffValue, OpenClosedType trueOpenClosedValue) throws IncompleteAccessoryException {
312 return new BooleanItemReader(
313 getItem(characteristicType, GenericItem.class)
314 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType)),
315 trueOnOffValue, trueOpenClosedValue);
319 * create boolean reader for a number item with ON state mapped to the value of the
320 * item being above a given threshold
322 * @param characteristicType characteristic id
323 * @param trueThreshold threshold for true of number item
324 * @param invertThreshold result is true if item is less than threshold, instead of more
325 * @return boolean read
326 * @throws IncompleteAccessoryException
329 protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType,
330 BigDecimal trueThreshold, boolean invertThreshold) throws IncompleteAccessoryException {
331 final HomekitTaggedItem taggedItem = getCharacteristic(characteristicType)
332 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType));
333 return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
334 taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN, trueThreshold, invertThreshold);
338 * create boolean reader with default ON/OFF mapping considering inverted flag
340 * @param characteristicType characteristic id
341 * @return boolean reader
342 * @throws IncompleteAccessoryException
345 protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType)
346 throws IncompleteAccessoryException {
347 final HomekitTaggedItem taggedItem = getCharacteristic(characteristicType)
348 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType));
349 return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
350 taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN);