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.math.RoundingMode;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
21 import java.util.Map.Entry;
22 import java.util.Optional;
23 import java.util.concurrent.CompletableFuture;
25 import javax.measure.Quantity;
26 import javax.measure.Unit;
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.library.unit.ImperialUnits;
36 import org.openhab.core.library.unit.SIUnits;
37 import org.openhab.core.types.State;
38 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
39 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
40 import org.openhab.io.homekit.internal.HomekitSettings;
41 import org.openhab.io.homekit.internal.HomekitTaggedItem;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import io.github.hapjava.accessories.HomekitAccessory;
46 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
47 import io.github.hapjava.services.Service;
50 * Abstract class for Homekit Accessory implementations, this provides the
51 * accessory metadata using information from the underlying Item.
53 * @author Andy Lintner - Initial contribution
55 abstract class AbstractHomekitAccessoryImpl implements HomekitAccessory {
56 private final Logger logger = LoggerFactory.getLogger(AbstractHomekitAccessoryImpl.class);
57 private final List<HomekitTaggedItem> characteristics;
58 private final HomekitTaggedItem accessory;
59 private final HomekitAccessoryUpdater updater;
60 private final HomekitSettings settings;
61 private final List<Service> services;
63 public AbstractHomekitAccessoryImpl(HomekitTaggedItem accessory, List<HomekitTaggedItem> characteristics,
64 HomekitAccessoryUpdater updater, HomekitSettings settings) {
65 this.characteristics = characteristics;
66 this.accessory = accessory;
67 this.updater = updater;
68 this.services = new ArrayList<>();
69 this.settings = settings;
73 protected Optional<HomekitTaggedItem> getCharacteristic(HomekitCharacteristicType type) {
74 return characteristics.stream().filter(c -> c.getCharacteristicType() == type).findAny();
79 return accessory.getId();
83 public CompletableFuture<String> getName() {
84 return CompletableFuture.completedFuture(accessory.getItem().getLabel());
88 public CompletableFuture<String> getManufacturer() {
89 return CompletableFuture.completedFuture("none");
93 public CompletableFuture<String> getModel() {
94 return CompletableFuture.completedFuture("none");
98 public CompletableFuture<String> getSerialNumber() {
99 return CompletableFuture.completedFuture(accessory.getItem().getName());
103 public CompletableFuture<String> getFirmwareRevision() {
104 return CompletableFuture.completedFuture("none");
108 public void identify() {
109 // We're not going to support this for now
112 public HomekitTaggedItem getRootAccessory() {
117 public Collection<Service> getServices() {
118 return this.services;
121 protected HomekitAccessoryUpdater getUpdater() {
125 protected HomekitSettings getSettings() {
130 protected void subscribe(HomekitCharacteristicType characteristicType,
131 HomekitCharacteristicChangeCallback callback) {
132 final Optional<HomekitTaggedItem> characteristic = getCharacteristic(characteristicType);
133 if (characteristic.isPresent()) {
134 getUpdater().subscribe((GenericItem) characteristic.get().getItem(), characteristicType.getTag(), callback);
136 logger.warn("Missing mandatory characteristic {}", characteristicType);
141 protected void unsubscribe(HomekitCharacteristicType characteristicType) {
142 final Optional<HomekitTaggedItem> characteristic = getCharacteristic(characteristicType);
143 if (characteristic.isPresent()) {
144 getUpdater().unsubscribe((GenericItem) characteristic.get().getItem(), characteristicType.getTag());
146 logger.warn("Missing mandatory characteristic {}", characteristicType);
150 protected @Nullable <T extends State> T getStateAs(HomekitCharacteristicType characteristic, Class<T> type) {
151 final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
152 if (taggedItem.isPresent()) {
153 final State state = taggedItem.get().getItem().getStateAs(type);
155 return state.as(type);
158 logger.debug("State for characteristic {} at accessory {} cannot be retrieved.", characteristic,
159 accessory.getName());
164 protected <T extends Item> Optional<T> getItem(HomekitCharacteristicType characteristic, Class<T> type) {
165 final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(characteristic);
166 if (taggedItem.isPresent()) {
167 final Item item = taggedItem.get().getItem();
168 if (type.isInstance(item)) {
169 return Optional.of((T) item);
171 logger.warn("Unsupported item type for characteristic {} at accessory {}. Expected {}, got {}",
172 characteristic, accessory.getItem().getName(), type, taggedItem.get().getItem().getClass());
175 logger.warn("Mandatory characteristic {} not found at accessory {}. ", characteristic,
176 accessory.getItem().getName());
179 return Optional.empty();
183 * return configuration attached to the root accessory, e.g. groupItem.
184 * Note: result will be casted to the type of the default value.
185 * The type for number is BigDecimal.
187 * @param key configuration key
188 * @param defaultValue default value
189 * @param <T> expected type
190 * @return configuration value
193 protected <T> T getAccessoryConfiguration(String key, T defaultValue) {
194 return accessory.getConfiguration(key, defaultValue);
198 * return configuration of the characteristic item, e.g. currentTemperature.
199 * Note: result will be casted to the type of the default value.
200 * The type for number is BigDecimal.
202 * @param characteristicType characteristic type
203 * @param key configuration key
204 * @param defaultValue default value
205 * @param <T> expected type
206 * @return configuration value
209 protected <T> T getAccessoryConfiguration(HomekitCharacteristicType characteristicType, String key,
211 return getCharacteristic(characteristicType)
212 .map(homekitTaggedItem -> homekitTaggedItem.getConfiguration(key, defaultValue)).orElse(defaultValue);
216 * update mapping with values from item configuration.
217 * it checks for all keys from the mapping whether there is configuration at item with the same key and if yes,
220 * @param characteristicType characteristicType to identify item
221 * @param map mapping to update
222 * @param customEnumList list to store custom state enumeration
225 protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map,
226 @Nullable List<T> customEnumList) {
227 getCharacteristic(characteristicType).ifPresent(c -> {
228 final Map<String, Object> configuration = c.getConfiguration();
229 if (configuration != null) {
230 map.forEach((k, current_value) -> {
231 final Object new_value = configuration.get(k.toString());
232 if (new_value instanceof String) {
233 map.put(k, (String) new_value);
234 if (customEnumList != null) {
235 customEnumList.add(k);
244 protected <T> void updateMapping(HomekitCharacteristicType characteristicType, Map<T, String> map) {
245 updateMapping(characteristicType, map, null);
249 * takes item state as value and retrieves the key for that value from mapping.
250 * e.g. used to map StringItem value to HomeKit Enum
252 * @param characteristicType characteristicType to identify item
253 * @param mapping mapping
254 * @param defaultValue default value if nothing found in mapping
255 * @param <T> type of the result derived from
256 * @return key for the value
259 protected <T> T getKeyFromMapping(HomekitCharacteristicType characteristicType, Map<T, String> mapping,
261 final Optional<HomekitTaggedItem> c = getCharacteristic(characteristicType);
263 final State state = c.get().getItem().getState();
264 logger.trace("getKeyFromMapping: characteristic {}, state {}, mapping {}", characteristicType.getTag(),
266 if (state instanceof StringType) {
267 return mapping.entrySet().stream().filter(entry -> state.toString().equalsIgnoreCase(entry.getValue()))
268 .findAny().map(Entry::getKey).orElseGet(() -> {
270 "Wrong value {} for {} characteristic of the item {}. Expected one of following {}. Returning {}.",
271 state.toString(), characteristicType.getTag(), c.get().getName(), mapping.values(),
281 protected void addCharacteristic(HomekitTaggedItem characteristic) {
282 characteristics.add(characteristic);
286 private <T extends Quantity<T>> double convertAndRound(double value, Unit<T> from, Unit<T> to) {
287 double rawValue = from.equals(to) ? value : from.getConverterTo(to).convert(value);
288 return new BigDecimal(rawValue).setScale(1, RoundingMode.HALF_UP).doubleValue();
292 protected double convertToCelsius(double degrees) {
293 return convertAndRound(degrees,
294 getSettings().useFahrenheitTemperature ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS, SIUnits.CELSIUS);
298 protected double convertFromCelsius(double degrees) {
299 return convertAndRound(degrees,
300 getSettings().useFahrenheitTemperature ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT,
301 ImperialUnits.FAHRENHEIT);
305 * create boolean reader with ON state mapped to trueOnOffValue or trueOpenClosedValue depending of item type
307 * @param characteristicType characteristic id
308 * @param trueOnOffValue ON value for switch
309 * @param trueOpenClosedValue ON value for contact
310 * @return boolean readed
311 * @throws IncompleteAccessoryException
314 protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType,
315 OnOffType trueOnOffValue, OpenClosedType trueOpenClosedValue) throws IncompleteAccessoryException {
316 return new BooleanItemReader(
317 getItem(characteristicType, GenericItem.class)
318 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType)),
319 trueOnOffValue, trueOpenClosedValue);
323 * create boolean reader with default ON/OFF mapping considering inverted flag
325 * @param characteristicType characteristic id
326 * @return boolean reader
327 * @throws IncompleteAccessoryException
330 protected BooleanItemReader createBooleanReader(HomekitCharacteristicType characteristicType)
331 throws IncompleteAccessoryException {
332 final HomekitTaggedItem taggedItem = getCharacteristic(characteristicType)
333 .orElseThrow(() -> new IncompleteAccessoryException(characteristicType));
334 return new BooleanItemReader(taggedItem.getItem(), taggedItem.isInverted() ? OnOffType.OFF : OnOffType.ON,
335 taggedItem.isInverted() ? OpenClosedType.CLOSED : OpenClosedType.OPEN);