2 * Copyright (c) 2010-2023 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.dto;
15 import static org.openhab.binding.hdpowerview.internal.dto.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 percentArg) {
79 int percent = percentArg;
80 switch (posKindCoords) {
81 case PRIMARY_POSITION:
83 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
85 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
86 // on dual rail shades constrain percent to not move the lower rail above the upper
87 State secondary = getState(shadeCapabilities, SECONDARY_POSITION);
88 if (secondary instanceof PercentType) {
89 int secPercent = ((PercentType) secondary).intValue();
90 if (percent < secPercent) {
95 posKind1 = posKindCoords.ordinal();
96 position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
99 case SECONDARY_POSITION:
101 * Secondary, blackout shade a 'Duolite' shade: => INVERTED
102 * Secondary, upper rail of a dual action shade: => NOT INVERTED
104 posKind1 = posKindCoords.ordinal();
105 if (shadeCapabilities.supportsSecondaryOverlapped()) {
106 position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
108 position1 = (int) Math.round((double) percent / 100 * MAX_SHADE);
112 case VANE_TILT_POSITION:
114 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
116 posKind1 = posKindCoords.ordinal();
117 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
118 position1 = (int) Math.round((double) percent / 100 * max);
122 posKind1 = CoordinateSystem.NONE.ordinal();
128 * Get the shade's position1 State for the given actuator class resp. coordinate system.
130 * @param shadeCapabilities the shade Thing capabilities.
131 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
132 * @return the State (or UNDEF if not available).
134 private State getPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
135 switch (posKindCoords) {
136 case PRIMARY_POSITION:
138 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
140 if (posKindCoords.equals(posKind1)) {
141 return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
143 if (VANE_TILT_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
144 return PercentType.HUNDRED;
146 if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()) {
147 return PercentType.HUNDRED;
151 case SECONDARY_POSITION:
153 * Secondary, blackout shade a 'Duolite' shade: => INVERTED
154 * Secondary, upper rail of a dual action shade: => NOT INVERTED
156 if (posKindCoords.equals(posKind1)) {
157 if (shadeCapabilities.supportsSecondaryOverlapped()) {
158 return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
160 return new PercentType((int) Math.round((double) position1 / MAX_SHADE * 100));
162 if (!SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()) {
163 return PercentType.ZERO;
167 case VANE_TILT_POSITION:
169 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
171 * If the shades are not open, the vane position is undefined; if the the shades
172 * are exactly open then the vanes are at zero; otherwise return the actual vane
175 * note: sometimes the hub may return a value of position1 > MAX_VANE (seems to
176 * be a bug in the hub) so we avoid an out of range exception via the Math.min()
179 if (posKindCoords.equals(posKind1)) {
180 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
181 return new PercentType((int) Math.round((double) Math.min(position1, max) / max * 100));
183 if (PRIMARY_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
184 return position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO;
186 if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()
187 && shadeCapabilities.supportsTiltOnClosed()) {
188 return PercentType.HUNDRED;
194 // fall through, return UNDEF
196 return UnDefType.UNDEF;
200 * Set the shade's position2 value for the given actuator class resp. coordinate system.
202 * @param shadeCapabilities the shade Thing capabilities.
203 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
204 * @param percent the new position value.
206 private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percentArg) {
207 int percent = percentArg;
208 switch (posKindCoords) {
209 case PRIMARY_POSITION:
211 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
213 posKind2 = posKindCoords.ordinal();
214 position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE));
217 case SECONDARY_POSITION:
219 * Secondary, upper rail of a dual action shade: => NOT INVERTED
221 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
222 // on dual rail shades constrain percent to not move the upper rail below the lower
223 State primary = getState(shadeCapabilities, PRIMARY_POSITION);
224 if (primary instanceof PercentType) {
225 int primaryPercent = ((PercentType) primary).intValue();
226 if (percent > primaryPercent) {
227 percent = primaryPercent;
231 posKind2 = posKindCoords.ordinal();
232 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE));
235 case VANE_TILT_POSITION:
236 posKind2 = posKindCoords.ordinal();
237 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
238 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * max));
248 * Get the shade's position2 State for the given actuator class resp. coordinate system.
250 * @param shadeCapabilities the shade Thing capabilities.
251 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
252 * @return the State (or UNDEF if not available).
254 private State getPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
255 Integer posKind2 = this.posKind2;
256 Integer position2 = this.position2;
258 if (position2 == null || posKind2 == null) {
259 return UnDefType.UNDEF;
262 switch (posKindCoords) {
263 case PRIMARY_POSITION:
265 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
267 if (posKindCoords.equals(posKind2)) {
268 return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
272 case SECONDARY_POSITION:
274 * Secondary, upper rail of a dual action shade: => NOT INVERTED
276 if (posKindCoords.equals(posKind2)) {
277 return new PercentType((int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
282 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
284 case VANE_TILT_POSITION:
285 if (posKindCoords.equals(posKind2)) {
286 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
287 return new PercentType((int) Math.round((double) Math.min(position2.intValue(), max) / max * 100));
293 // fall through, return UNDEF
295 return UnDefType.UNDEF;
299 * Detect if the ShadePosition has a posKindN value indicating potential support for a secondary rail.
301 * @return true if the ShadePosition supports a secondary rail.
303 public boolean secondaryRailDetected() {
304 return SECONDARY_POSITION.equals(posKind1) || SECONDARY_POSITION.equals(posKind2);
308 * Detect if the ShadePosition has both a posKindN value indicating potential support for tilt, AND a posKindN
309 * indicating support for a primary rail. i.e. it potentially supports tilt anywhere functionality.
311 * @return true if potential support for tilt anywhere functionality was detected.
313 public boolean tiltAnywhereDetected() {
314 return ((PRIMARY_POSITION.equals(posKind1)) && (VANE_TILT_POSITION.equals(posKind2))
315 || ((PRIMARY_POSITION.equals(posKind2) && (VANE_TILT_POSITION.equals(posKind1)))));
319 * Set the shade's position for the given actuator class resp. coordinate system.
321 * @param shadeCapabilities the shade Thing capabilities.
322 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
323 * @param percent the new position value.
324 * @return this object.
326 public ShadePosition setPosition(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
327 logger.trace("setPosition(): capabilities={}, coords={}, percent={}", shadeCapabilities, posKindCoords,
329 // if necessary swap the order of position1 and position2
330 if (PRIMARY_POSITION.equals(posKind2) && !PRIMARY_POSITION.equals(posKind1)) {
331 final Integer posKind2Temp = posKind2;
332 final Integer position2Temp = position2;
333 posKind2 = Integer.valueOf(posKind1);
334 position2 = Integer.valueOf(position1);
335 posKind1 = posKind2Temp != null ? posKind2Temp.intValue() : NONE.ordinal();
336 position1 = position2Temp != null ? position2Temp.intValue() : 0;
339 // delete position2 if it has an invalid position kind
340 if (ERROR_UNKNOWN.equals(posKind2) || NONE.equals(posKind2)) {
345 // logic to set either position1 or position2
346 switch (posKindCoords) {
347 case PRIMARY_POSITION:
348 if (shadeCapabilities.supportsPrimary()) {
349 setPosition1(shadeCapabilities, posKindCoords, percent);
353 case SECONDARY_POSITION:
354 if (shadeCapabilities.supportsSecondary()) {
355 if (shadeCapabilities.supportsPrimary()) {
356 setPosition2(shadeCapabilities, posKindCoords, percent);
358 setPosition1(shadeCapabilities, posKindCoords, percent);
360 } else if (shadeCapabilities.supportsSecondaryOverlapped()) {
361 setPosition1(shadeCapabilities, posKindCoords, percent);
365 case VANE_TILT_POSITION:
366 if (shadeCapabilities.supportsPrimary()) {
367 if (shadeCapabilities.supportsTiltOnClosed()) {
368 setPosition1(shadeCapabilities, posKindCoords, percent);
369 } else if (shadeCapabilities.supportsTiltAnywhere()) {
370 setPosition2(shadeCapabilities, posKindCoords, percent);
372 } else if (shadeCapabilities.supportsTiltAnywhere()) {
373 setPosition1(shadeCapabilities, posKindCoords, percent);
379 // fall through, do nothing