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