]> git.basschouten.com Git - openhab-addons.git/blob
8f8b250b8f4c0cae795a3f0659439f20e282b6a7
[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.binding.hdpowerview.internal.dto;
14
15 import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*;
16
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;
25
26 /**
27  * The position of a single shade, as returned by the HD PowerView hub
28  *
29  * @author Andy Lintner - Initial contribution
30  * @author Andrew Fiddian-Green - Added support for secondary rail positions
31  */
32 @NonNullByDefault
33 public class ShadePosition {
34
35     private final transient Logger logger = LoggerFactory.getLogger(ShadePosition.class);
36
37     /**
38      * Primary actuator position.
39      */
40     private int posKind1;
41     private int position1;
42
43     /**
44      * Secondary actuator position.
45      *
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.
48      */
49     private @Nullable Integer posKind2 = null;
50     private @Nullable Integer position2 = null;
51
52     public ShadePosition() {
53     }
54
55     /**
56      * Get the shade's State for the given actuator class resp. coordinate system.
57      *
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.
61      */
62     public State getState(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
63         State result = getPosition1(shadeCapabilities, posKindCoords);
64         if (result == UnDefType.UNDEF) {
65             result = getPosition2(shadeCapabilities, posKindCoords);
66         }
67         logger.trace("getState(): capabilities={}, coords={} => result={}", shadeCapabilities, posKindCoords, result);
68         return result;
69     }
70
71     /**
72      * Set the shade's position1 value for the given actuator class resp. coordinate system.
73      *
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.
77      */
78     private void setPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percentArg) {
79         int percent = percentArg;
80         switch (posKindCoords) {
81             case PRIMARY_POSITION:
82                 /*
83                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
84                  */
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 percentCommand) {
89                         int secPercent = percentCommand.intValue();
90                         if (percent < secPercent) {
91                             percent = secPercent;
92                         }
93                     }
94                 }
95                 posKind1 = posKindCoords.ordinal();
96                 position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
97                 break;
98
99             case SECONDARY_POSITION:
100                 /*
101                  * Secondary, blackout shade a 'Duolite' shade: => INVERTED
102                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
103                  */
104                 posKind1 = posKindCoords.ordinal();
105                 if (shadeCapabilities.supportsSecondaryOverlapped()) {
106                     position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
107                 } else {
108                     position1 = (int) Math.round((double) percent / 100 * MAX_SHADE);
109                 }
110                 break;
111
112             case VANE_TILT_POSITION:
113                 /*
114                  * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
115                  */
116                 posKind1 = posKindCoords.ordinal();
117                 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
118                 position1 = (int) Math.round((double) percent / 100 * max);
119                 break;
120
121             default:
122                 posKind1 = CoordinateSystem.NONE.ordinal();
123                 position1 = 0;
124         }
125     }
126
127     /**
128      * Get the shade's position1 State for the given actuator class resp. coordinate system.
129      *
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).
133      */
134     private State getPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
135         switch (posKindCoords) {
136             case PRIMARY_POSITION:
137                 /*
138                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
139                  */
140                 if (posKindCoords.equals(posKind1)) {
141                     return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
142                 }
143                 if (VANE_TILT_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
144                     return PercentType.HUNDRED;
145                 }
146                 if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()) {
147                     return PercentType.HUNDRED;
148                 }
149                 break;
150
151             case SECONDARY_POSITION:
152                 /*
153                  * Secondary, blackout shade a 'Duolite' shade: => INVERTED
154                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
155                  */
156                 if (posKindCoords.equals(posKind1)) {
157                     if (shadeCapabilities.supportsSecondaryOverlapped()) {
158                         return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
159                     }
160                     return new PercentType((int) Math.round((double) position1 / MAX_SHADE * 100));
161                 }
162                 if (!SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()) {
163                     return PercentType.ZERO;
164                 }
165                 break;
166
167             case VANE_TILT_POSITION:
168                 /*
169                  * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
170                  *
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
173                  * position itself
174                  *
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()
177                  * function below..
178                  */
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));
182                 }
183                 if (PRIMARY_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
184                     return position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO;
185                 }
186                 if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsSecondaryOverlapped()
187                         && shadeCapabilities.supportsTiltOnClosed()) {
188                     return PercentType.HUNDRED;
189                 }
190                 break;
191
192             case ERROR_UNKNOWN:
193             case NONE:
194                 // fall through, return UNDEF
195         }
196         return UnDefType.UNDEF;
197     }
198
199     /**
200      * Set the shade's position2 value for the given actuator class resp. coordinate system.
201      *
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.
205      */
206     private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percentArg) {
207         int percent = percentArg;
208         switch (posKindCoords) {
209             case PRIMARY_POSITION:
210                 /*
211                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
212                  */
213                 posKind2 = posKindCoords.ordinal();
214                 position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE));
215                 break;
216
217             case SECONDARY_POSITION:
218                 /*
219                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
220                  */
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 percentCommand) {
225                         int primaryPercent = percentCommand.intValue();
226                         if (percent > primaryPercent) {
227                             percent = primaryPercent;
228                         }
229                     }
230                 }
231                 posKind2 = posKindCoords.ordinal();
232                 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE));
233                 break;
234
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));
239                 break;
240
241             default:
242                 posKind2 = null;
243                 position2 = null;
244         }
245     }
246
247     /**
248      * Get the shade's position2 State for the given actuator class resp. coordinate system.
249      *
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).
253      */
254     private State getPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
255         Integer posKind2 = this.posKind2;
256         Integer position2 = this.position2;
257
258         if (position2 == null || posKind2 == null) {
259             return UnDefType.UNDEF;
260         }
261
262         switch (posKindCoords) {
263             case PRIMARY_POSITION:
264                 /*
265                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
266                  */
267                 if (posKindCoords.equals(posKind2)) {
268                     return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
269                 }
270                 break;
271
272             case SECONDARY_POSITION:
273                 /*
274                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
275                  */
276                 if (posKindCoords.equals(posKind2)) {
277                     return new PercentType((int) Math.round(position2.doubleValue() / MAX_SHADE * 100));
278                 }
279                 break;
280
281             /*
282              * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
283              */
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));
288                 }
289                 break;
290
291             case ERROR_UNKNOWN:
292             case NONE:
293                 // fall through, return UNDEF
294         }
295         return UnDefType.UNDEF;
296     }
297
298     /**
299      * Detect if the ShadePosition has a posKindN value indicating potential support for a secondary rail.
300      *
301      * @return true if the ShadePosition supports a secondary rail.
302      */
303     public boolean secondaryRailDetected() {
304         return SECONDARY_POSITION.equals(posKind1) || SECONDARY_POSITION.equals(posKind2);
305     }
306
307     /**
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.
310      *
311      * @return true if potential support for tilt anywhere functionality was detected.
312      */
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)))));
316     }
317
318     /**
319      * Set the shade's position for the given actuator class resp. coordinate system.
320      *
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.
325      */
326     public ShadePosition setPosition(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
327         logger.trace("setPosition(): capabilities={}, coords={}, percent={}", shadeCapabilities, posKindCoords,
328                 percent);
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;
337         }
338
339         // delete position2 if it has an invalid position kind
340         if (ERROR_UNKNOWN.equals(posKind2) || NONE.equals(posKind2)) {
341             posKind2 = null;
342             position2 = null;
343         }
344
345         // logic to set either position1 or position2
346         switch (posKindCoords) {
347             case PRIMARY_POSITION:
348                 if (shadeCapabilities.supportsPrimary()) {
349                     setPosition1(shadeCapabilities, posKindCoords, percent);
350                 }
351                 break;
352
353             case SECONDARY_POSITION:
354                 if (shadeCapabilities.supportsSecondary()) {
355                     if (shadeCapabilities.supportsPrimary()) {
356                         setPosition2(shadeCapabilities, posKindCoords, percent);
357                     } else {
358                         setPosition1(shadeCapabilities, posKindCoords, percent);
359                     }
360                 } else if (shadeCapabilities.supportsSecondaryOverlapped()) {
361                     setPosition1(shadeCapabilities, posKindCoords, percent);
362                 }
363                 break;
364
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);
371                     }
372                 } else if (shadeCapabilities.supportsTiltAnywhere()) {
373                     setPosition1(shadeCapabilities, posKindCoords, percent);
374                 }
375                 break;
376
377             case ERROR_UNKNOWN:
378             case NONE:
379                 // fall through, do nothing
380         }
381         return this;
382     }
383 }