2 * Copyright (c) 2010-2024 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 static org.openhab.io.homekit.internal.HomekitCharacteristicType.CURRENT_POSITION;
16 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.POSITION_STATE;
17 import static org.openhab.io.homekit.internal.HomekitCharacteristicType.TARGET_POSITION;
19 import java.util.List;
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.GroupItem;
27 import org.openhab.core.items.Item;
28 import org.openhab.core.library.items.DimmerItem;
29 import org.openhab.core.library.items.NumberItem;
30 import org.openhab.core.library.items.RollershutterItem;
31 import org.openhab.core.library.types.DecimalType;
32 import org.openhab.core.library.types.PercentType;
33 import org.openhab.core.library.types.StopMoveType;
34 import org.openhab.core.library.types.UpDownType;
35 import org.openhab.io.homekit.internal.HomekitAccessoryUpdater;
36 import org.openhab.io.homekit.internal.HomekitCharacteristicType;
37 import org.openhab.io.homekit.internal.HomekitSettings;
38 import org.openhab.io.homekit.internal.HomekitTaggedItem;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import io.github.hapjava.characteristics.Characteristic;
43 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
44 import io.github.hapjava.characteristics.impl.windowcovering.PositionStateEnum;
47 * Common methods for Door, Window and WindowCovering.
49 * @author Eugen Freiter - Initial contribution
52 abstract class AbstractHomekitPositionAccessoryImpl extends AbstractHomekitAccessoryImpl {
53 private final Logger logger = LoggerFactory.getLogger(AbstractHomekitPositionAccessoryImpl.class);
54 protected int closedPosition;
55 protected int openPosition;
56 private final Map<PositionStateEnum, String> positionStateMapping;
57 protected boolean emulateState;
58 protected boolean emulateStopSameDirection;
59 protected boolean sendUpDownForExtents;
60 protected PositionStateEnum emulatedState = PositionStateEnum.STOPPED;
62 public AbstractHomekitPositionAccessoryImpl(HomekitTaggedItem taggedItem,
63 List<HomekitTaggedItem> mandatoryCharacteristics, List<Characteristic> mandatoryRawCharacteristics,
64 HomekitAccessoryUpdater updater, HomekitSettings settings) {
65 super(taggedItem, mandatoryCharacteristics, mandatoryRawCharacteristics, updater, settings);
66 final boolean inverted = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.INVERTED, true);
67 emulateState = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.EMULATE_STOP_STATE, false);
68 emulateStopSameDirection = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.EMULATE_STOP_SAME_DIRECTION,
70 sendUpDownForExtents = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.SEND_UP_DOWN_FOR_EXTENTS, false);
71 closedPosition = inverted ? 0 : 100;
72 openPosition = inverted ? 100 : 0;
73 positionStateMapping = createMapping(POSITION_STATE, PositionStateEnum.class);
76 public CompletableFuture<Integer> getCurrentPosition() {
77 return CompletableFuture.completedFuture(convertPositionState(CURRENT_POSITION, openPosition, closedPosition));
80 public CompletableFuture<PositionStateEnum> getPositionState() {
81 return CompletableFuture.completedFuture(emulateState ? emulatedState
82 : getKeyFromMapping(POSITION_STATE, positionStateMapping, PositionStateEnum.STOPPED));
85 public CompletableFuture<Integer> getTargetPosition() {
86 return CompletableFuture.completedFuture(convertPositionState(TARGET_POSITION, openPosition, closedPosition));
89 public CompletableFuture<Void> setTargetPosition(int value) {
90 getCharacteristic(TARGET_POSITION).ifPresentOrElse(taggedItem -> {
91 final Item item = taggedItem.getItem();
92 final int targetPosition = convertPosition(value, openPosition);
93 if (item instanceof RollershutterItem itemAsRollerShutterItem) {
94 // HomeKit home app never sends STOP. we emulate stop if we receive 100% or 0% while the blind is moving
95 if (emulateState && (targetPosition == 100 && emulatedState == PositionStateEnum.DECREASING)
96 || ((targetPosition == 0 && emulatedState == PositionStateEnum.INCREASING))) {
97 if (emulateStopSameDirection) {
98 // some blinds devices do not support "STOP" but would stop if receive UP/DOWN while moving
99 itemAsRollerShutterItem
100 .send(emulatedState == PositionStateEnum.INCREASING ? UpDownType.UP : UpDownType.DOWN);
102 itemAsRollerShutterItem.send(StopMoveType.STOP);
104 emulatedState = PositionStateEnum.STOPPED;
106 if (sendUpDownForExtents && targetPosition == 0) {
107 itemAsRollerShutterItem.send(UpDownType.UP);
108 } else if (sendUpDownForExtents && targetPosition == 100) {
109 itemAsRollerShutterItem.send(UpDownType.DOWN);
111 itemAsRollerShutterItem.send(new PercentType(targetPosition));
115 PercentType currentPosition = item.getStateAs(PercentType.class);
116 emulatedState = currentPosition == null || currentPosition.intValue() == targetPosition
117 ? PositionStateEnum.STOPPED
118 : currentPosition.intValue() < targetPosition ? PositionStateEnum.INCREASING
119 : PositionStateEnum.DECREASING;
122 } else if (item instanceof DimmerItem itemAsDimmerItem) {
123 itemAsDimmerItem.send(new PercentType(targetPosition));
124 } else if (item instanceof NumberItem itemAsNumberItem) {
125 itemAsNumberItem.send(new DecimalType(targetPosition));
126 } else if (item instanceof GroupItem itemAsGroupItem
127 && itemAsGroupItem.getBaseItem() instanceof RollershutterItem) {
128 itemAsGroupItem.send(new PercentType(targetPosition));
129 } else if (item instanceof GroupItem itemAsGroupItem
130 && itemAsGroupItem.getBaseItem() instanceof DimmerItem) {
131 itemAsGroupItem.send(new PercentType(targetPosition));
132 } else if (item instanceof GroupItem itemAsGroupItem
133 && itemAsGroupItem.getBaseItem() instanceof NumberItem) {
134 itemAsGroupItem.send(new DecimalType(targetPosition));
137 "Unsupported item type for characteristic {} at accessory {}. Expected Rollershutter, Dimmer or Number item, got {}",
138 TARGET_POSITION, getName(), item.getClass());
141 logger.warn("Mandatory characteristic {} not found at accessory {}. ", TARGET_POSITION, getName());
143 return CompletableFuture.completedFuture(null);
146 public void subscribeCurrentPosition(HomekitCharacteristicChangeCallback callback) {
147 subscribe(CURRENT_POSITION, callback);
150 public void subscribePositionState(HomekitCharacteristicChangeCallback callback) {
151 subscribe(POSITION_STATE, callback);
154 public void subscribeTargetPosition(HomekitCharacteristicChangeCallback callback) {
155 subscribe(TARGET_POSITION, callback);
158 public void unsubscribeCurrentPosition() {
159 unsubscribe(CURRENT_POSITION);
162 public void unsubscribePositionState() {
163 unsubscribe(POSITION_STATE);
166 public void unsubscribeTargetPosition() {
167 unsubscribe(TARGET_POSITION);
171 * convert/invert position of door/window/blinds.
172 * openHAB Rollershutter is:
173 * - completely open if position is 0%,
174 * - completely closed if position is 100%.
175 * HomeKit mapping has inverted mapping
176 * From Specification: "For blinds/shades/awnings, a value of 0 indicates a position that permits the least light
178 * of 100 indicates a position that allows most light.", i.e.
180 * - completely open if position is 100%,
181 * - completely closed if position is 0%.
183 * As openHAB rollershutter item is typically used for window covering, the binding has by default inverting
185 * One can override this default behaviour with inverted="false/no" flag. in this cases, openHAB item value will be
186 * sent to HomeKit with no changes.
188 * @param value source value
189 * @return target value
191 protected int convertPosition(int value, int openPosition) {
192 return Math.abs(openPosition - value);
195 protected int convertPositionState(HomekitCharacteristicType type, int openPosition, int closedPosition) {
197 DecimalType value = null;
198 final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(type);
199 if (taggedItem.isPresent()) {
200 final Item item = taggedItem.get().getItem();
201 final Item baseItem = taggedItem.get().getBaseItem();
202 if (baseItem instanceof RollershutterItem || baseItem instanceof DimmerItem) {
203 value = item.getStateAs(PercentType.class);
205 value = item.getStateAs(DecimalType.class);
208 return value != null ? convertPosition(value.intValue(), openPosition) : closedPosition;