]> git.basschouten.com Git - openhab-addons.git/blob
694b83fb2dd4f210ea5f1afc7c5b0e419b48dd4f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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 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;
18
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.concurrent.CompletableFuture;
23
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;
41
42 import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
43 import io.github.hapjava.characteristics.impl.windowcovering.PositionStateEnum;
44
45 /**
46  * Common methods for Door, Window and WindowCovering.
47  * 
48  * @author Eugen Freiter - Initial contribution
49  */
50 @NonNullByDefault
51 abstract class AbstractHomekitPositionAccessoryImpl extends AbstractHomekitAccessoryImpl {
52     private final Logger logger = LoggerFactory.getLogger(AbstractHomekitPositionAccessoryImpl.class);
53     protected int closedPosition;
54     protected int openPosition;
55     private final Map<PositionStateEnum, String> positionStateMapping;
56     protected boolean emulateState;
57     protected boolean emulateStopSameDirection;
58     protected PositionStateEnum emulatedState = PositionStateEnum.STOPPED;
59
60     public AbstractHomekitPositionAccessoryImpl(HomekitTaggedItem taggedItem,
61             List<HomekitTaggedItem> mandatoryCharacteristics, HomekitAccessoryUpdater updater,
62             HomekitSettings settings) {
63         super(taggedItem, mandatoryCharacteristics, updater, settings);
64         final boolean inverted = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.INVERTED, true);
65         emulateState = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.EMULATE_STOP_STATE, false);
66         emulateStopSameDirection = getAccessoryConfigurationAsBoolean(HomekitTaggedItem.EMULATE_STOP_SAME_DIRECTION,
67                 false);
68         closedPosition = inverted ? 0 : 100;
69         openPosition = inverted ? 100 : 0;
70         positionStateMapping = createMapping(POSITION_STATE, PositionStateEnum.class);
71     }
72
73     public CompletableFuture<Integer> getCurrentPosition() {
74         return CompletableFuture.completedFuture(convertPositionState(CURRENT_POSITION, openPosition, closedPosition));
75     }
76
77     public CompletableFuture<PositionStateEnum> getPositionState() {
78         return CompletableFuture.completedFuture(emulateState ? emulatedState
79                 : getKeyFromMapping(POSITION_STATE, positionStateMapping, PositionStateEnum.STOPPED));
80     }
81
82     public CompletableFuture<Integer> getTargetPosition() {
83         return CompletableFuture.completedFuture(convertPositionState(TARGET_POSITION, openPosition, closedPosition));
84     }
85
86     public CompletableFuture<Void> setTargetPosition(int value) {
87         getCharacteristic(TARGET_POSITION).ifPresentOrElse(taggedItem -> {
88             final Item item = taggedItem.getItem();
89             final int targetPosition = convertPosition(value, openPosition);
90             if (item instanceof RollershutterItem itemAsRollerShutterItem) {
91                 // HomeKit home app never sends STOP. we emulate stop if we receive 100% or 0% while the blind is moving
92                 if (emulateState && (targetPosition == 100 && emulatedState == PositionStateEnum.DECREASING)
93                         || ((targetPosition == 0 && emulatedState == PositionStateEnum.INCREASING))) {
94                     if (emulateStopSameDirection) {
95                         // some blinds devices do not support "STOP" but would stop if receive UP/DOWN while moving
96                         itemAsRollerShutterItem
97                                 .send(emulatedState == PositionStateEnum.INCREASING ? UpDownType.UP : UpDownType.DOWN);
98                     } else {
99                         itemAsRollerShutterItem.send(StopMoveType.STOP);
100                     }
101                     emulatedState = PositionStateEnum.STOPPED;
102                 } else {
103                     itemAsRollerShutterItem.send(new PercentType(targetPosition));
104                     if (emulateState) {
105                         @Nullable
106                         PercentType currentPosition = item.getStateAs(PercentType.class);
107                         emulatedState = currentPosition == null || currentPosition.intValue() == targetPosition
108                                 ? PositionStateEnum.STOPPED
109                                 : currentPosition.intValue() < targetPosition ? PositionStateEnum.INCREASING
110                                         : PositionStateEnum.DECREASING;
111                     }
112                 }
113             } else if (item instanceof DimmerItem itemAsDimmerItem) {
114                 itemAsDimmerItem.send(new PercentType(targetPosition));
115             } else if (item instanceof NumberItem itemAsNumberItem) {
116                 itemAsNumberItem.send(new DecimalType(targetPosition));
117             } else if (item instanceof GroupItem itemAsGroupItem
118                     && itemAsGroupItem.getBaseItem() instanceof RollershutterItem) {
119                 itemAsGroupItem.send(new PercentType(targetPosition));
120             } else if (item instanceof GroupItem itemAsGroupItem
121                     && itemAsGroupItem.getBaseItem() instanceof DimmerItem) {
122                 itemAsGroupItem.send(new PercentType(targetPosition));
123             } else if (item instanceof GroupItem itemAsGroupItem
124                     && itemAsGroupItem.getBaseItem() instanceof NumberItem) {
125                 itemAsGroupItem.send(new DecimalType(targetPosition));
126             } else {
127                 logger.warn(
128                         "Unsupported item type for characteristic {} at accessory {}. Expected Rollershutter, Dimmer or Number item, got {}",
129                         TARGET_POSITION, getName(), item.getClass());
130             }
131         }, () -> {
132             logger.warn("Mandatory characteristic {} not found at accessory {}. ", TARGET_POSITION, getName());
133         });
134         return CompletableFuture.completedFuture(null);
135     }
136
137     public void subscribeCurrentPosition(HomekitCharacteristicChangeCallback callback) {
138         subscribe(CURRENT_POSITION, callback);
139     }
140
141     public void subscribePositionState(HomekitCharacteristicChangeCallback callback) {
142         subscribe(POSITION_STATE, callback);
143     }
144
145     public void subscribeTargetPosition(HomekitCharacteristicChangeCallback callback) {
146         subscribe(TARGET_POSITION, callback);
147     }
148
149     public void unsubscribeCurrentPosition() {
150         unsubscribe(CURRENT_POSITION);
151     }
152
153     public void unsubscribePositionState() {
154         unsubscribe(POSITION_STATE);
155     }
156
157     public void unsubscribeTargetPosition() {
158         unsubscribe(TARGET_POSITION);
159     }
160
161     /**
162      * convert/invert position of door/window/blinds.
163      * openHAB Rollershutter is:
164      * - completely open if position is 0%,
165      * - completely closed if position is 100%.
166      * HomeKit mapping has inverted mapping
167      * From Specification: "For blinds/shades/awnings, a value of 0 indicates a position that permits the least light
168      * and a value
169      * of 100 indicates a position that allows most light.", i.e.
170      * HomeKit Blinds is
171      * - completely open if position is 100%,
172      * - completely closed if position is 0%.
173      *
174      * As openHAB rollershutter item is typically used for window covering, the binding has by default inverting
175      * mapping.
176      * One can override this default behaviour with inverted="false/no" flag. in this cases, openHAB item value will be
177      * sent to HomeKit with no changes.
178      *
179      * @param value source value
180      * @return target value
181      */
182     protected int convertPosition(int value, int openPosition) {
183         return Math.abs(openPosition - value);
184     }
185
186     protected int convertPositionState(HomekitCharacteristicType type, int openPosition, int closedPosition) {
187         @Nullable
188         DecimalType value = null;
189         final Optional<HomekitTaggedItem> taggedItem = getCharacteristic(type);
190         if (taggedItem.isPresent()) {
191             final Item item = taggedItem.get().getItem();
192             final Item baseItem = taggedItem.get().getBaseItem();
193             if (baseItem instanceof RollershutterItem || baseItem instanceof DimmerItem) {
194                 value = item.getStateAs(PercentType.class);
195             } else {
196                 value = item.getStateAs(DecimalType.class);
197             }
198         }
199         return value != null ? convertPosition(value.intValue(), openPosition) : closedPosition;
200     }
201 }