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_ZERO_IS_CLOSED:
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_ZERO_IS_OPEN);
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_ZERO_IS_OPEN:
100 * Secondary, upper rail of a dual action shade: => NOT INVERTED
102 posKind1 = posKindCoords.ordinal();
103 position1 = (int) Math.round((double) percent / 100 * MAX_SHADE);
106 case VANE_TILT_COORDS:
108 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
110 posKind1 = posKindCoords.ordinal();
111 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
112 position1 = (int) Math.round((double) percent / 100 * max);
116 posKind1 = CoordinateSystem.NONE.ordinal();
122 * Get the shade's position1 State for the given actuator class resp. coordinate system.
124 * @param shadeCapabilities the shade Thing capabilities.
125 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
126 * @return the State (or UNDEF if not available).
128 private State getPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
129 switch (posKindCoords) {
130 case PRIMARY_ZERO_IS_CLOSED:
132 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
134 if (posKindCoords.equals(posKind1)) {
135 return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
137 if (VANE_TILT_COORDS.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
138 return PercentType.HUNDRED;
142 case SECONDARY_ZERO_IS_OPEN:
144 * Secondary, upper rail of a dual action shade: => NOT INVERTED
146 if (posKindCoords.equals(posKind1)) {
147 return new PercentType((int) Math.round((double) position1 / MAX_SHADE * 100));
151 case VANE_TILT_COORDS:
153 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
155 * If the shades are not open, the vane position is undefined; if the the shades
156 * are exactly open then the vanes are at zero; otherwise return the actual vane
159 * note: sometimes the hub may return a value of position1 > MAX_VANE (seems to
160 * be a bug in the hub) so we avoid an out of range exception via the Math.min()
163 if (posKindCoords.equals(posKind1)) {
164 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
165 return new PercentType((int) Math.round((double) Math.min(position1, max) / max * 100));
167 if (PRIMARY_ZERO_IS_CLOSED.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
168 return position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO;
174 // fall through, return UNDEF
176 return UnDefType.UNDEF;
180 * Set the shade's position2 value for the given actuator class resp. coordinate system.
182 * @param shadeCapabilities the shade Thing capabilities.
183 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
184 * @param percent the new position value.
186 private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
187 switch (posKindCoords) {
188 case PRIMARY_ZERO_IS_CLOSED:
190 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
192 posKind2 = posKindCoords.ordinal();
193 position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE));
196 case SECONDARY_ZERO_IS_OPEN:
198 * Secondary, upper rail of a dual action shade: => NOT INVERTED
200 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
201 // on dual rail shades constrain percent to not move the upper rail below the lower
202 State primary = getState(shadeCapabilities, PRIMARY_ZERO_IS_CLOSED);
203 if (primary instanceof PercentType) {
204 int primaryPercent = ((PercentType) primary).intValue();
205 if (percent > primaryPercent) {
206 percent = primaryPercent;
210 posKind2 = posKindCoords.ordinal();
211 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE));
214 case VANE_TILT_COORDS:
215 posKind2 = posKindCoords.ordinal();
216 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
217 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * max));
227 * Get the shade's position2 State for the given actuator class resp. coordinate system.
229 * @param shadeCapabilities the shade Thing capabilities.
230 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
231 * @return the State (or UNDEF if not available).
233 private State getPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
234 Integer posKind2 = this.posKind2;
235 Integer position2 = this.position2;
237 if (position2 == null || posKind2 == null) {
238 return UnDefType.UNDEF;
241 switch (posKindCoords) {
242 case PRIMARY_ZERO_IS_CLOSED:
244 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
246 if (posKindCoords.equals(posKind2)) {
247 return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
251 case SECONDARY_ZERO_IS_OPEN:
253 * Secondary, upper rail of a dual action shade: => NOT INVERTED
255 if (posKindCoords.equals(posKind2)) {
256 return new PercentType((int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
261 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
263 * note: sometimes the hub may return a value of position1 > MAX_VANE (seems to
264 * be a bug in the hub) so we avoid an out of range exception via the Math.min()
267 case VANE_TILT_COORDS:
268 if (posKindCoords.equals(posKind2)) {
269 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
270 return new PercentType((int) Math.round((double) Math.min(position2.intValue(), max) / max * 100));
276 // fall through, return UNDEF
278 return UnDefType.UNDEF;
282 * Detect if the ShadePosition has a posKindN value indicating potential support for a secondary rail.
284 * @return true if the ShadePosition supports a secondary rail.
286 public boolean secondaryRailDetected() {
287 return SECONDARY_ZERO_IS_OPEN.equals(posKind1) || SECONDARY_ZERO_IS_OPEN.equals(posKind2);
291 * Detect if the ShadePosition has both a posKindN value indicating potential support for tilt, AND a posKindN
292 * indicating support for a primary rail. i.e. it potentially supports tilt anywhere functionality.
294 * @return true if potential support for tilt anywhere functionality was detected.
296 public boolean tiltAnywhereDetected() {
297 return ((PRIMARY_ZERO_IS_CLOSED.equals(posKind1)) && (VANE_TILT_COORDS.equals(posKind2))
298 || ((PRIMARY_ZERO_IS_CLOSED.equals(posKind2) && (VANE_TILT_COORDS.equals(posKind1)))));
302 * Set the shade's position for the given actuator class resp. coordinate system.
304 * @param shadeCapabilities the shade Thing capabilities.
305 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
306 * @param percent the new position value.
307 * @return this object.
309 public ShadePosition setPosition(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
310 logger.trace("setPosition(): capabilities={}, coords={}, percent={}", shadeCapabilities, posKindCoords,
312 // if necessary swap the order of position1 and position2
313 if (PRIMARY_ZERO_IS_CLOSED.equals(posKind2) && !PRIMARY_ZERO_IS_CLOSED.equals(posKind1)) {
314 final Integer posKind2Temp = posKind2;
315 final Integer position2Temp = position2;
316 posKind2 = Integer.valueOf(posKind1);
317 position2 = Integer.valueOf(position1);
318 posKind1 = posKind2Temp != null ? posKind2Temp.intValue() : NONE.ordinal();
319 position1 = position2Temp != null ? position2Temp.intValue() : 0;
322 // delete position2 if it has an invalid position kind
323 if (ERROR_UNKNOWN.equals(posKind2) || NONE.equals(posKind2)) {
328 // logic to set either position1 or position2
329 switch (posKindCoords) {
330 case PRIMARY_ZERO_IS_CLOSED:
331 if (shadeCapabilities.supportsPrimary()) {
332 setPosition1(shadeCapabilities, posKindCoords, percent);
336 case SECONDARY_ZERO_IS_OPEN:
337 if (shadeCapabilities.supportsSecondary()) {
338 if (shadeCapabilities.supportsPrimary()) {
339 setPosition2(shadeCapabilities, posKindCoords, percent);
341 setPosition1(shadeCapabilities, posKindCoords, percent);
346 case VANE_TILT_COORDS:
347 if (shadeCapabilities.supportsPrimary()) {
348 if (shadeCapabilities.supportsTiltOnClosed()) {
349 setPosition1(shadeCapabilities, posKindCoords, percent);
350 } else if (shadeCapabilities.supportsTiltAnywhere()) {
351 setPosition2(shadeCapabilities, posKindCoords, percent);
353 } else if (shadeCapabilities.supportsTiltAnywhere()) {
354 setPosition1(shadeCapabilities, posKindCoords, percent);
360 // fall through, do nothing