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.binding.hdpowerview.internal.api;
15 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
17 import org.eclipse.jdt.annotation.NonNullByDefault;
18 import org.eclipse.jdt.annotation.Nullable;
19 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
20 import org.openhab.core.library.types.PercentType;
21 import org.openhab.core.types.State;
22 import org.openhab.core.types.UnDefType;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
27 * The position of a single shade, as returned by the HD PowerView hub
29 * @author Andy Lintner - Initial contribution
30 * @author Andrew Fiddian-Green - Added support for secondary rail positions
33 public class ShadePosition {
35 private final transient Logger logger = LoggerFactory.getLogger(ShadePosition.class);
38 * Primary actuator position.
41 private int position1;
44 * Secondary actuator position.
46 * Here we have to use Integer objects rather than just int primitives because these are secondary optional position
47 * elements in the JSON payload, so the GSON de-serializer might leave them as null.
49 private @Nullable Integer posKind2 = null;
50 private @Nullable Integer position2 = null;
52 public ShadePosition() {
56 * Get the shade's State for the given actuator class resp. coordinate system.
58 * @param shadeCapabilities the shade Thing capabilities.
59 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
60 * @return the current state.
62 public State getState(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
63 State result = getPosition1(shadeCapabilities, posKindCoords);
64 if (result == UnDefType.UNDEF) {
65 result = getPosition2(shadeCapabilities, posKindCoords);
67 logger.trace("getState(): capabilities={}, coords={} => result={}", shadeCapabilities, posKindCoords, result);
72 * Set the shade's position1 value for the given actuator class resp. coordinate system.
74 * @param shadeCapabilities the shade Thing capabilities.
75 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
76 * @param percent the new position value.
78 private void setPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
79 switch (posKindCoords) {
80 case PRIMARY_POSITION:
82 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
84 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
85 // on dual rail shades constrain percent to not move the lower rail above the upper
86 State secondary = getState(shadeCapabilities, SECONDARY_POSITION);
87 if (secondary instanceof PercentType) {
88 int secPercent = ((PercentType) secondary).intValue();
89 if (percent < secPercent) {
94 posKind1 = posKindCoords.ordinal();
95 position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
98 case SECONDARY_POSITION:
100 * Secondary, blackout shade a 'Duolite' shade: => INVERTED
101 * Secondary, upper rail of a dual action shade: => NOT INVERTED
103 posKind1 = posKindCoords.ordinal();
104 if (shadeCapabilities.supportsSecondaryOverlapped()) {
105 position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
107 position1 = (int) Math.round((double) percent / 100 * MAX_SHADE);
111 case VANE_TILT_POSITION:
113 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
115 posKind1 = posKindCoords.ordinal();
116 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
117 position1 = (int) Math.round((double) percent / 100 * max);
121 posKind1 = CoordinateSystem.NONE.ordinal();
127 * Get the shade's position1 State for the given actuator class resp. coordinate system.
129 * @param shadeCapabilities the shade Thing capabilities.
130 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
131 * @return the State (or UNDEF if not available).
133 private State getPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
134 switch (posKindCoords) {
135 case PRIMARY_POSITION:
137 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
139 if (posKindCoords.equals(posKind1)) {
140 return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
142 if (VANE_TILT_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
143 return PercentType.HUNDRED;
145 if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()) {
146 return PercentType.HUNDRED;
150 case SECONDARY_POSITION:
152 * Secondary, blackout shade a 'Duolite' shade: => INVERTED
153 * Secondary, upper rail of a dual action shade: => NOT INVERTED
155 if (posKindCoords.equals(posKind1)) {
156 if (shadeCapabilities.supportsSecondaryOverlapped()) {
157 return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
159 return new PercentType((int) Math.round((double) position1 / MAX_SHADE * 100));
161 if (PRIMARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()) {
162 return PercentType.ZERO;
166 case VANE_TILT_POSITION:
168 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
170 * If the shades are not open, the vane position is undefined; if the the shades
171 * are exactly open then the vanes are at zero; otherwise return the actual vane
174 * note: sometimes the hub may return a value of position1 > MAX_VANE (seems to
175 * be a bug in the hub) so we avoid an out of range exception via the Math.min()
178 if (posKindCoords.equals(posKind1)) {
179 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
180 return new PercentType((int) Math.round((double) Math.min(position1, max) / max * 100));
182 if (PRIMARY_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
183 return position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO;
189 // fall through, return UNDEF
191 return UnDefType.UNDEF;
195 * Set the shade's position2 value for the given actuator class resp. coordinate system.
197 * @param shadeCapabilities the shade Thing capabilities.
198 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
199 * @param percent the new position value.
201 private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
202 switch (posKindCoords) {
203 case PRIMARY_POSITION:
205 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
207 posKind2 = posKindCoords.ordinal();
208 position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE));
211 case SECONDARY_POSITION:
213 * Secondary, upper rail of a dual action shade: => NOT INVERTED
215 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
216 // on dual rail shades constrain percent to not move the upper rail below the lower
217 State primary = getState(shadeCapabilities, PRIMARY_POSITION);
218 if (primary instanceof PercentType) {
219 int primaryPercent = ((PercentType) primary).intValue();
220 if (percent > primaryPercent) {
221 percent = primaryPercent;
225 posKind2 = posKindCoords.ordinal();
226 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE));
229 case VANE_TILT_POSITION:
230 posKind2 = posKindCoords.ordinal();
231 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
232 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * max));
242 * Get the shade's position2 State for the given actuator class resp. coordinate system.
244 * @param shadeCapabilities the shade Thing capabilities.
245 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
246 * @return the State (or UNDEF if not available).
248 private State getPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
249 Integer posKind2 = this.posKind2;
250 Integer position2 = this.position2;
252 if (position2 == null || posKind2 == null) {
253 return UnDefType.UNDEF;
256 switch (posKindCoords) {
257 case PRIMARY_POSITION:
259 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
261 if (posKindCoords.equals(posKind2)) {
262 return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
266 case SECONDARY_POSITION:
268 * Secondary, upper rail of a dual action shade: => NOT INVERTED
270 if (posKindCoords.equals(posKind2)) {
271 return new PercentType((int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
276 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
278 case VANE_TILT_POSITION:
279 if (posKindCoords.equals(posKind2)) {
280 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
281 return new PercentType((int) Math.round((double) Math.min(position2.intValue(), max) / max * 100));
287 // fall through, return UNDEF
289 return UnDefType.UNDEF;
293 * Detect if the ShadePosition has a posKindN value indicating potential support for a secondary rail.
295 * @return true if the ShadePosition supports a secondary rail.
297 public boolean secondaryRailDetected() {
298 return SECONDARY_POSITION.equals(posKind1) || SECONDARY_POSITION.equals(posKind2);
302 * Detect if the ShadePosition has both a posKindN value indicating potential support for tilt, AND a posKindN
303 * indicating support for a primary rail. i.e. it potentially supports tilt anywhere functionality.
305 * @return true if potential support for tilt anywhere functionality was detected.
307 public boolean tiltAnywhereDetected() {
308 return ((PRIMARY_POSITION.equals(posKind1)) && (VANE_TILT_POSITION.equals(posKind2))
309 || ((PRIMARY_POSITION.equals(posKind2) && (VANE_TILT_POSITION.equals(posKind1)))));
313 * Set the shade's position for the given actuator class resp. coordinate system.
315 * @param shadeCapabilities the shade Thing capabilities.
316 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
317 * @param percent the new position value.
318 * @return this object.
320 public ShadePosition setPosition(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
321 logger.trace("setPosition(): capabilities={}, coords={}, percent={}", shadeCapabilities, posKindCoords,
323 // if necessary swap the order of position1 and position2
324 if (PRIMARY_POSITION.equals(posKind2) && !PRIMARY_POSITION.equals(posKind1)) {
325 final Integer posKind2Temp = posKind2;
326 final Integer position2Temp = position2;
327 posKind2 = Integer.valueOf(posKind1);
328 position2 = Integer.valueOf(position1);
329 posKind1 = posKind2Temp != null ? posKind2Temp.intValue() : NONE.ordinal();
330 position1 = position2Temp != null ? position2Temp.intValue() : 0;
333 // delete position2 if it has an invalid position kind
334 if (ERROR_UNKNOWN.equals(posKind2) || NONE.equals(posKind2)) {
339 // logic to set either position1 or position2
340 switch (posKindCoords) {
341 case PRIMARY_POSITION:
342 if (shadeCapabilities.supportsPrimary()) {
343 setPosition1(shadeCapabilities, posKindCoords, percent);
347 case SECONDARY_POSITION:
348 if (shadeCapabilities.supportsSecondary()) {
349 if (shadeCapabilities.supportsPrimary()) {
350 setPosition2(shadeCapabilities, posKindCoords, percent);
352 setPosition1(shadeCapabilities, posKindCoords, percent);
354 } else if (shadeCapabilities.supportsSecondaryOverlapped()) {
355 setPosition1(shadeCapabilities, posKindCoords, percent);
359 case VANE_TILT_POSITION:
360 if (shadeCapabilities.supportsPrimary()) {
361 if (shadeCapabilities.supportsTiltOnClosed()) {
362 setPosition1(shadeCapabilities, posKindCoords, percent);
363 } else if (shadeCapabilities.supportsTiltAnywhere()) {
364 setPosition2(shadeCapabilities, posKindCoords, percent);
366 } else if (shadeCapabilities.supportsTiltAnywhere()) {
367 setPosition1(shadeCapabilities, posKindCoords, percent);
373 // fall through, do nothing