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 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 (!SECONDARY_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;
185 if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()
186 && shadeCapabilities.supportsTiltOnClosed()) {
187 return PercentType.HUNDRED;
193 // fall through, return UNDEF
195 return UnDefType.UNDEF;
199 * Set the shade's position2 value for the given actuator class resp. coordinate system.
201 * @param shadeCapabilities the shade Thing capabilities.
202 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
203 * @param percent the new position value.
205 private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
206 switch (posKindCoords) {
207 case PRIMARY_POSITION:
209 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
211 posKind2 = posKindCoords.ordinal();
212 position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE));
215 case SECONDARY_POSITION:
217 * Secondary, upper rail of a dual action shade: => NOT INVERTED
219 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
220 // on dual rail shades constrain percent to not move the upper rail below the lower
221 State primary = getState(shadeCapabilities, PRIMARY_POSITION);
222 if (primary instanceof PercentType) {
223 int primaryPercent = ((PercentType) primary).intValue();
224 if (percent > primaryPercent) {
225 percent = primaryPercent;
229 posKind2 = posKindCoords.ordinal();
230 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE));
233 case VANE_TILT_POSITION:
234 posKind2 = posKindCoords.ordinal();
235 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
236 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * max));
246 * Get the shade's position2 State for the given actuator class resp. coordinate system.
248 * @param shadeCapabilities the shade Thing capabilities.
249 * @param posKindCoords the actuator class (coordinate system) whose state is to be returned.
250 * @return the State (or UNDEF if not available).
252 private State getPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
253 Integer posKind2 = this.posKind2;
254 Integer position2 = this.position2;
256 if (position2 == null || posKind2 == null) {
257 return UnDefType.UNDEF;
260 switch (posKindCoords) {
261 case PRIMARY_POSITION:
263 * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
265 if (posKindCoords.equals(posKind2)) {
266 return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
270 case SECONDARY_POSITION:
272 * Secondary, upper rail of a dual action shade: => NOT INVERTED
274 if (posKindCoords.equals(posKind2)) {
275 return new PercentType((int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
280 * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
282 case VANE_TILT_POSITION:
283 if (posKindCoords.equals(posKind2)) {
284 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
285 return new PercentType((int) Math.round((double) Math.min(position2.intValue(), max) / max * 100));
291 // fall through, return UNDEF
293 return UnDefType.UNDEF;
297 * Detect if the ShadePosition has a posKindN value indicating potential support for a secondary rail.
299 * @return true if the ShadePosition supports a secondary rail.
301 public boolean secondaryRailDetected() {
302 return SECONDARY_POSITION.equals(posKind1) || SECONDARY_POSITION.equals(posKind2);
306 * Detect if the ShadePosition has both a posKindN value indicating potential support for tilt, AND a posKindN
307 * indicating support for a primary rail. i.e. it potentially supports tilt anywhere functionality.
309 * @return true if potential support for tilt anywhere functionality was detected.
311 public boolean tiltAnywhereDetected() {
312 return ((PRIMARY_POSITION.equals(posKind1)) && (VANE_TILT_POSITION.equals(posKind2))
313 || ((PRIMARY_POSITION.equals(posKind2) && (VANE_TILT_POSITION.equals(posKind1)))));
317 * Set the shade's position for the given actuator class resp. coordinate system.
319 * @param shadeCapabilities the shade Thing capabilities.
320 * @param posKindCoords the actuator class (coordinate system) whose state is to be changed.
321 * @param percent the new position value.
322 * @return this object.
324 public ShadePosition setPosition(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
325 logger.trace("setPosition(): capabilities={}, coords={}, percent={}", shadeCapabilities, posKindCoords,
327 // if necessary swap the order of position1 and position2
328 if (PRIMARY_POSITION.equals(posKind2) && !PRIMARY_POSITION.equals(posKind1)) {
329 final Integer posKind2Temp = posKind2;
330 final Integer position2Temp = position2;
331 posKind2 = Integer.valueOf(posKind1);
332 position2 = Integer.valueOf(position1);
333 posKind1 = posKind2Temp != null ? posKind2Temp.intValue() : NONE.ordinal();
334 position1 = position2Temp != null ? position2Temp.intValue() : 0;
337 // delete position2 if it has an invalid position kind
338 if (ERROR_UNKNOWN.equals(posKind2) || NONE.equals(posKind2)) {
343 // logic to set either position1 or position2
344 switch (posKindCoords) {
345 case PRIMARY_POSITION:
346 if (shadeCapabilities.supportsPrimary()) {
347 setPosition1(shadeCapabilities, posKindCoords, percent);
351 case SECONDARY_POSITION:
352 if (shadeCapabilities.supportsSecondary()) {
353 if (shadeCapabilities.supportsPrimary()) {
354 setPosition2(shadeCapabilities, posKindCoords, percent);
356 setPosition1(shadeCapabilities, posKindCoords, percent);
358 } else if (shadeCapabilities.supportsSecondaryOverlapped()) {
359 setPosition1(shadeCapabilities, posKindCoords, percent);
363 case VANE_TILT_POSITION:
364 if (shadeCapabilities.supportsPrimary()) {
365 if (shadeCapabilities.supportsTiltOnClosed()) {
366 setPosition1(shadeCapabilities, posKindCoords, percent);
367 } else if (shadeCapabilities.supportsTiltAnywhere()) {
368 setPosition2(shadeCapabilities, posKindCoords, percent);
370 } else if (shadeCapabilities.supportsTiltAnywhere()) {
371 setPosition1(shadeCapabilities, posKindCoords, percent);
377 // fall through, do nothing