]> git.basschouten.com Git - openhab-addons.git/commitdiff
[hdpowerview] Added support for rear blackout panel (#12098)
authorAndrew Fiddian-Green <software@whitebear.ch>
Tue, 25 Jan 2022 19:09:14 +0000 (19:09 +0000)
committerGitHub <noreply@github.com>
Tue, 25 Jan 2022 19:09:14 +0000 (20:09 +0100)
* [hdpowerview] refactor enum constant names
* [hdpowerview] add support for blackout shades
* [hdpowerview] unit tests for capabilities 8 & 9
* [hdpowerview] delete no longer valid comment
* [hdpowerview] blackout shade position is never UNDEF
* [hdpowerview] updated read me
* [hdpowerview] refactor unit tests into two classes

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
bundles/org.openhab.binding.hdpowerview/README.md
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/database/ShadeCapabilitiesDatabase.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java
bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java
bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/ShadePositionTest.java [new file with mode: 0644]

index c777915c4681e4b23ac3c7a9746ff6d63d882b40..694e812983dab7949ccc533af4d067d3cf78fa57 100644 (file)
@@ -111,7 +111,7 @@ The `position` and `secondary` channels are Rollershutter types.
 For vertical shades, the binding maps the vertical position of the "rail" to the Rollershutter ▲ / ▼ commands, and its respective percent value.
 And for horizontal shades, it maps the horizontal position of the "truck" to the Rollershutter ▲ / ▼ commands, and its respective percent value.
 
-Depending on whether the shade is a top-down, bottom-up, left-right, right-left, or dual action shade, the `OPEN` and `CLOSED` position of the shades may differ from the ▲ / ▼ commands follows..
+Depending on whether the shade is a top-down, bottom-up, left-right, right-left, dual action shade, or, a shade with a secondary blackout panel, the `OPEN` and `CLOSED` position of the shades may differ from the ▲ / ▼ commands follows..
 
 | Type of Shade               | Channel           | Rollershutter Command | Motion direction | Shade State    | Percent           | Pebble Remote Button |
 |-----------------------------|-------------------|-----------------------|------------------|----------------|-------------------|----------------------|
@@ -127,6 +127,8 @@ Depending on whether the shade is a top-down, bottom-up, left-right, right-left,
 |                             |                   | ▼                     | Down             | `CLOSED`       | 100%              | ▼                    |
 | Dual action<br>(upper rail) | ***`secondary`*** | ▲                     | Up               | ***`CLOSED`*** | 0%<sup>1)</sup>   | ![](doc/right.png)   |
 |                             |                   | ▼                     | Down             | ***`OPEN`***   | 100%<sup>1)</sup> | ![](doc/left.png)    |
+| Blackout panel ('DuoLite')  | ***`secondary`*** | ▲                     | Up               | `OPEN`         | 0%                | ▲                    |
+|                             |                   | ▼                     | Down             | `CLOSED`       | 100%              | ▼                    |
 
 ***<sup>1)</sup> BUG NOTE***: In openHAB versions v3.1.x and earlier, there was a bug in the handling of the position percent value of the `secondary` shade.
 Although the RollerShutter Up/Down commands functioned properly as described in the table above, the percent state values (e.g. displayed on a slider control), did not.
@@ -159,6 +161,9 @@ On dual action shades, the top rail cannot move below the bottom rail, nor can t
 So the value of `secondary` is constrained by the prior value of `position`.
 And the value of `position` is constrained by the prior value of `secondary`.
 
+On shades with a secondary blackout panel 'DuoLite', the secondary blackout panel cannot be moved unless the main shade panel is already down.
+In this case, the position of the secondary blackout panel is reported as 0%.
+
 ## Refreshing the PowerView Hub Cache
 
 The hub maintains a cache of the last known state of its shades, and this binding delivers those values.
index 46db0371a999836c18d3f54026a4186ac5924291..236e468947820353ed3495588fe1315c8570156d 100644 (file)
@@ -19,9 +19,9 @@ import org.eclipse.jdt.annotation.Nullable;
  * Shade coordinate system (a.k.a. position kind), as returned by the HD PowerView hub.
  *
  * @param NONE a coordinate system that does not refer to any type of physical rail.
- * @param PRIMARY_ZERO_IS_CLOSED primary rail, whose coordinate value 0 means shade is closed.
- * @param SECONDARY_ZERO_IS_OPEN secondary rail, whose coordinate value 0 means shade is open.
- * @param VANE_TILT_COORDS vane/tilt operator, whose coordinate system is for vanes.
+ * @param PRIMARY_POSITION primary rail, whose coordinate value 0 means shade is closed.
+ * @param SECONDARY_POSITION secondary rail, whose coordinate value 0 means shade is open.
+ * @param VANE_TILT_POSITION vane/tilt operator, whose coordinate system is for vanes.
  * @param ERROR_UNKNOWN unsupported coordinate system.
  *
  * @author Andy Lintner - Initial contribution of the original enum called
@@ -77,9 +77,9 @@ public enum CoordinateSystem {
      *
      */
     NONE,
-    PRIMARY_ZERO_IS_CLOSED,
-    SECONDARY_ZERO_IS_OPEN,
-    VANE_TILT_COORDS,
+    PRIMARY_POSITION,
+    SECONDARY_POSITION,
+    VANE_TILT_POSITION,
     ERROR_UNKNOWN;
 
     public static final int MAX_SHADE = 65535;
index 0c32427e4a61eae1e1edb6e0eb5e3617e3f78635..1c397d60bbac465abd313c71df64e49bdcc9926b 100644 (file)
@@ -77,13 +77,13 @@ public class ShadePosition {
      */
     private void setPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
         switch (posKindCoords) {
-            case PRIMARY_ZERO_IS_CLOSED:
+            case PRIMARY_POSITION:
                 /*
                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
                  */
                 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
                     // on dual rail shades constrain percent to not move the lower rail above the upper
-                    State secondary = getState(shadeCapabilities, SECONDARY_ZERO_IS_OPEN);
+                    State secondary = getState(shadeCapabilities, SECONDARY_POSITION);
                     if (secondary instanceof PercentType) {
                         int secPercent = ((PercentType) secondary).intValue();
                         if (percent < secPercent) {
@@ -95,15 +95,20 @@ public class ShadePosition {
                 position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
                 break;
 
-            case SECONDARY_ZERO_IS_OPEN:
+            case SECONDARY_POSITION:
                 /*
+                 * Secondary, blackout shade a 'Duolite' shade: => INVERTED
                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
                  */
                 posKind1 = posKindCoords.ordinal();
-                position1 = (int) Math.round((double) percent / 100 * MAX_SHADE);
+                if (shadeCapabilities.supportsBlackoutShade()) {
+                    position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE);
+                } else {
+                    position1 = (int) Math.round((double) percent / 100 * MAX_SHADE);
+                }
                 break;
 
-            case VANE_TILT_COORDS:
+            case VANE_TILT_POSITION:
                 /*
                  * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
                  */
@@ -127,28 +132,38 @@ public class ShadePosition {
      */
     private State getPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) {
         switch (posKindCoords) {
-            case PRIMARY_ZERO_IS_CLOSED:
+            case PRIMARY_POSITION:
                 /*
                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
                  */
                 if (posKindCoords.equals(posKind1)) {
                     return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
                 }
-                if (VANE_TILT_COORDS.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
+                if (VANE_TILT_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
+                    return PercentType.HUNDRED;
+                }
+                if (SECONDARY_POSITION.equals(posKind1) && shadeCapabilities.supportsBlackoutShade()) {
                     return PercentType.HUNDRED;
                 }
                 break;
 
-            case SECONDARY_ZERO_IS_OPEN:
+            case SECONDARY_POSITION:
                 /*
+                 * Secondary, blackout shade a 'Duolite' shade: => INVERTED
                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
                  */
                 if (posKindCoords.equals(posKind1)) {
+                    if (shadeCapabilities.supportsBlackoutShade()) {
+                        return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100));
+                    }
                     return new PercentType((int) Math.round((double) position1 / MAX_SHADE * 100));
                 }
+                if (PRIMARY_POSITION.equals(posKind1) && shadeCapabilities.supportsBlackoutShade()) {
+                    return PercentType.ZERO;
+                }
                 break;
 
-            case VANE_TILT_COORDS:
+            case VANE_TILT_POSITION:
                 /*
                  * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
                  *
@@ -164,7 +179,7 @@ public class ShadePosition {
                     int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
                     return new PercentType((int) Math.round((double) Math.min(position1, max) / max * 100));
                 }
-                if (PRIMARY_ZERO_IS_CLOSED.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
+                if (PRIMARY_POSITION.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) {
                     return position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO;
                 }
                 break;
@@ -185,7 +200,7 @@ public class ShadePosition {
      */
     private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) {
         switch (posKindCoords) {
-            case PRIMARY_ZERO_IS_CLOSED:
+            case PRIMARY_POSITION:
                 /*
                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
                  */
@@ -193,13 +208,13 @@ public class ShadePosition {
                 position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE));
                 break;
 
-            case SECONDARY_ZERO_IS_OPEN:
+            case SECONDARY_POSITION:
                 /*
                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
                  */
                 if (shadeCapabilities.supportsPrimary() && shadeCapabilities.supportsSecondary()) {
                     // on dual rail shades constrain percent to not move the upper rail below the lower
-                    State primary = getState(shadeCapabilities, PRIMARY_ZERO_IS_CLOSED);
+                    State primary = getState(shadeCapabilities, PRIMARY_POSITION);
                     if (primary instanceof PercentType) {
                         int primaryPercent = ((PercentType) primary).intValue();
                         if (percent > primaryPercent) {
@@ -211,7 +226,7 @@ public class ShadePosition {
                 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE));
                 break;
 
-            case VANE_TILT_COORDS:
+            case VANE_TILT_POSITION:
                 posKind2 = posKindCoords.ordinal();
                 int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
                 position2 = Integer.valueOf((int) Math.round((double) percent / 100 * max));
@@ -239,7 +254,7 @@ public class ShadePosition {
         }
 
         switch (posKindCoords) {
-            case PRIMARY_ZERO_IS_CLOSED:
+            case PRIMARY_POSITION:
                 /*
                  * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED
                  */
@@ -248,7 +263,7 @@ public class ShadePosition {
                 }
                 break;
 
-            case SECONDARY_ZERO_IS_OPEN:
+            case SECONDARY_POSITION:
                 /*
                  * Secondary, upper rail of a dual action shade: => NOT INVERTED
                  */
@@ -259,12 +274,8 @@ public class ShadePosition {
 
             /*
              * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED
-             *
-             * note: sometimes the hub may return a value of position1 > MAX_VANE (seems to
-             * be a bug in the hub) so we avoid an out of range exception via the Math.min()
-             * function below..
              */
-            case VANE_TILT_COORDS:
+            case VANE_TILT_POSITION:
                 if (posKindCoords.equals(posKind2)) {
                     int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE;
                     return new PercentType((int) Math.round((double) Math.min(position2.intValue(), max) / max * 100));
@@ -284,7 +295,7 @@ public class ShadePosition {
      * @return true if the ShadePosition supports a secondary rail.
      */
     public boolean secondaryRailDetected() {
-        return SECONDARY_ZERO_IS_OPEN.equals(posKind1) || SECONDARY_ZERO_IS_OPEN.equals(posKind2);
+        return SECONDARY_POSITION.equals(posKind1) || SECONDARY_POSITION.equals(posKind2);
     }
 
     /**
@@ -294,8 +305,8 @@ public class ShadePosition {
      * @return true if potential support for tilt anywhere functionality was detected.
      */
     public boolean tiltAnywhereDetected() {
-        return ((PRIMARY_ZERO_IS_CLOSED.equals(posKind1)) && (VANE_TILT_COORDS.equals(posKind2))
-                || ((PRIMARY_ZERO_IS_CLOSED.equals(posKind2) && (VANE_TILT_COORDS.equals(posKind1)))));
+        return ((PRIMARY_POSITION.equals(posKind1)) && (VANE_TILT_POSITION.equals(posKind2))
+                || ((PRIMARY_POSITION.equals(posKind2) && (VANE_TILT_POSITION.equals(posKind1)))));
     }
 
     /**
@@ -310,7 +321,7 @@ public class ShadePosition {
         logger.trace("setPosition(): capabilities={}, coords={}, percent={}", shadeCapabilities, posKindCoords,
                 percent);
         // if necessary swap the order of position1 and position2
-        if (PRIMARY_ZERO_IS_CLOSED.equals(posKind2) && !PRIMARY_ZERO_IS_CLOSED.equals(posKind1)) {
+        if (PRIMARY_POSITION.equals(posKind2) && !PRIMARY_POSITION.equals(posKind1)) {
             final Integer posKind2Temp = posKind2;
             final Integer position2Temp = position2;
             posKind2 = Integer.valueOf(posKind1);
@@ -327,23 +338,25 @@ public class ShadePosition {
 
         // logic to set either position1 or position2
         switch (posKindCoords) {
-            case PRIMARY_ZERO_IS_CLOSED:
+            case PRIMARY_POSITION:
                 if (shadeCapabilities.supportsPrimary()) {
                     setPosition1(shadeCapabilities, posKindCoords, percent);
                 }
                 break;
 
-            case SECONDARY_ZERO_IS_OPEN:
+            case SECONDARY_POSITION:
                 if (shadeCapabilities.supportsSecondary()) {
                     if (shadeCapabilities.supportsPrimary()) {
                         setPosition2(shadeCapabilities, posKindCoords, percent);
                     } else {
                         setPosition1(shadeCapabilities, posKindCoords, percent);
                     }
+                } else if (shadeCapabilities.supportsBlackoutShade()) {
+                    setPosition1(shadeCapabilities, posKindCoords, percent);
                 }
                 break;
 
-            case VANE_TILT_COORDS:
+            case VANE_TILT_POSITION:
                 if (shadeCapabilities.supportsPrimary()) {
                     if (shadeCapabilities.supportsTiltOnClosed()) {
                         setPosition1(shadeCapabilities, posKindCoords, percent);
index 1fcb7a529b87e23ce5051275289eff02bcdeadbc..a1f665c1aee5c09910f614639d3dc5ec5ebffe8a 100644 (file)
@@ -48,8 +48,8 @@ public class ShadeCapabilitiesDatabase {
             new Capabilities(5)          .tiltAnywhere().tilt180()            .text("Tilt Only 180°"),
             new Capabilities(6).primary()                                     .text("Top Down")                 .primaryStateInverted(),
             new Capabilities(7).primary()                         .secondary().text("Top Down Bottom Up"),
-            new Capabilities(8).primary()                                     .text("Duolite Lift"),
-            new Capabilities(9).primary().tiltAnywhere()                      .text("Duolite Lift and Tilt 90°"),
+            new Capabilities(8).primary()                                     .text("Duolite Lift")             .withBlackoutShade(),
+            new Capabilities(9).primary().tiltAnywhere()                      .text("Duolite Lift and Tilt 90°").withBlackoutShade(),
     // @formatter:on
             new Capabilities()).stream().collect(Collectors.toMap(Capabilities::getValue, Function.identity()));
 
@@ -148,12 +148,18 @@ public class ShadeCapabilitiesDatabase {
         private boolean supportsSecondary;
         private boolean supportsTiltOnClosed;
         private boolean supportsTiltAnywhere;
+        private boolean supportsBlackoutShade;
         private boolean primaryStateInverted;
         private boolean tilt180Degrees;
 
         public Capabilities() {
         }
 
+        protected Capabilities withBlackoutShade() {
+            supportsBlackoutShade = true;
+            return this;
+        }
+
         protected Capabilities(int capabilities) {
             intValue = capabilities;
         }
@@ -249,6 +255,15 @@ public class ShadeCapabilitiesDatabase {
         public boolean supportsTilt180() {
             return tilt180Degrees;
         }
+
+        /**
+         * Check if the Capabilities class instance supports a secondary 'DuoLite' blackout shade.
+         *
+         * @return true if the primary shade supports a secondary blackout shade.
+         */
+        public boolean supportsBlackoutShade() {
+            return supportsBlackoutShade;
+        }
     }
 
     /**
index 30ab73e7008561660b1661754b468cae1a6df285..428cb5f895079eefa0bb978788f013dd90c3e629 100644 (file)
@@ -192,9 +192,9 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
         switch (channelId) {
             case CHANNEL_SHADE_POSITION:
                 if (command instanceof PercentType) {
-                    moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue(), webTargets, shadeId);
+                    moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
                 } else if (command instanceof UpDownType) {
-                    moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
+                    moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
                 } else if (command instanceof StopMoveType) {
                     if (StopMoveType.STOP == command) {
                         stopShade(webTargets, shadeId);
@@ -206,17 +206,17 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
 
             case CHANNEL_SHADE_VANE:
                 if (command instanceof PercentType) {
-                    moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue(), webTargets, shadeId);
+                    moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
                 } else if (command instanceof OnOffType) {
-                    moveShade(VANE_TILT_COORDS, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
+                    moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
                 }
                 break;
 
             case CHANNEL_SHADE_SECONDARY_POSITION:
                 if (command instanceof PercentType) {
-                    moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue(), webTargets, shadeId);
+                    moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
                 } else if (command instanceof UpDownType) {
-                    moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
+                    moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
                 } else if (command instanceof StopMoveType) {
                     if (StopMoveType.STOP == command) {
                         stopShade(webTargets, shadeId);
@@ -392,9 +392,9 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
             return;
         }
-        updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED));
-        updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_COORDS));
-        updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN));
+        updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
+        updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
+        updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
     }
 
     private void updateBatteryLevelStates(int batteryStatus) {
index 229468ecb53d89e71eeebbedbe46d1668e74bc35..883124450d0cb98a50aff1189cf667226c410ae9 100644 (file)
@@ -57,8 +57,6 @@ public class HDPowerViewJUnitTests {
     private static final Pattern VALID_IP_V4_ADDRESS = Pattern
             .compile("\\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}\\b");
 
-    private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
-
     /*
      * load a test JSON string from a file.
      */
@@ -190,16 +188,17 @@ public class HDPowerViewJUnitTests {
                     assertNotNull(capabilitiesValue);
 
                     if (positions != null && capabilitiesValue != null) {
-                        Capabilities capabilities = db.getCapabilities(capabilitiesValue.intValue());
+                        Capabilities capabilities = new ShadeCapabilitiesDatabase()
+                                .getCapabilities(capabilitiesValue.intValue());
 
-                        State pos = positions.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
+                        State pos = positions.getState(capabilities, PRIMARY_POSITION);
                         assertEquals(PercentType.class, pos.getClass());
 
                         int position = ((PercentType) pos).intValue();
                         position = position + ((position <= 10) ? 5 : -5);
 
-                        ShadePosition targetPosition = new ShadePosition().setPosition(capabilities,
-                                PRIMARY_ZERO_IS_CLOSED, position);
+                        ShadePosition targetPosition = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION,
+                                position);
                         assertNotNull(targetPosition);
 
                         if (allowShadeMovementCommands) {
@@ -209,8 +208,8 @@ public class HDPowerViewJUnitTests {
                             ShadePosition actualPosition = newData.positions;
                             assertNotNull(actualPosition);
                             if (actualPosition != null) {
-                                assertEquals(targetPosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED),
-                                        actualPosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED));
+                                assertEquals(targetPosition.getState(capabilities, PRIMARY_POSITION),
+                                        actualPosition.getState(capabilities, PRIMARY_POSITION));
                             }
                         }
                     }
@@ -250,73 +249,6 @@ public class HDPowerViewJUnitTests {
         }
     }
 
-    /**
-     * Test parsing of ShadePosition (shade fully up).
-     *
-     */
-    @Test
-    public void testShadePositionParsingFullyUp() {
-        Capabilities capabilities = db.getCapabilities(0);
-        ShadePosition test = new ShadePosition().setPosition(capabilities, PRIMARY_ZERO_IS_CLOSED, 0);
-        assertNotNull(test);
-        State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(0, ((PercentType) pos).intValue());
-        pos = test.getState(capabilities, VANE_TILT_COORDS);
-        assertTrue(UnDefType.UNDEF.equals(pos));
-    }
-
-    /**
-     * Test parsing of ShadePosition (shade fully down (method 1)).
-     *
-     */
-    @Test
-    public void testShadePositionParsingShadeFullyDown1() {
-        Capabilities capabilities = db.getCapabilities(0);
-        ShadePosition test = new ShadePosition().setPosition(capabilities, PRIMARY_ZERO_IS_CLOSED, 100);
-        assertNotNull(test);
-        State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(100, ((PercentType) pos).intValue());
-        pos = test.getState(capabilities, VANE_TILT_COORDS);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(0, ((PercentType) pos).intValue());
-    }
-
-    /**
-     * Test parsing of ShadePosition (shade fully down (method 2)).
-     *
-     */
-    @Test
-    public void testShadePositionParsingShadeFullyDown2() {
-        Capabilities capabilities = db.getCapabilities(0);
-        ShadePosition test = new ShadePosition().setPosition(capabilities, VANE_TILT_COORDS, 0);
-        assertNotNull(test);
-        State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(100, ((PercentType) pos).intValue());
-        pos = test.getState(capabilities, VANE_TILT_COORDS);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(0, ((PercentType) pos).intValue());
-    }
-
-    /**
-     * Test parsing of ShadePosition (shade fully down (method 2) and vane fully open).
-     *
-     */
-    @Test
-    public void testShadePositionParsingShadeFullyDownVaneOpen() {
-        Capabilities capabilities = db.getCapabilities(0);
-        ShadePosition test = new ShadePosition().setPosition(capabilities, VANE_TILT_COORDS, 100);
-        assertNotNull(test);
-        State pos = test.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(100, ((PercentType) pos).intValue());
-        pos = test.getState(capabilities, VANE_TILT_COORDS);
-        assertEquals(PercentType.class, pos.getClass());
-        assertEquals(100, ((PercentType) pos).intValue());
-    }
-
     /**
      * Test generic JSON shades response.
      */
@@ -409,18 +341,18 @@ public class HDPowerViewJUnitTests {
                     assertNotNull(capabilitiesValue);
                     if (capabilitiesValue != null) {
                         assertEquals(7, capabilitiesValue.intValue());
-
+                        ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
                         Capabilities capabilities = db.getCapabilities(capabilitiesValue);
 
-                        State pos = shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
+                        State pos = shadePos.getState(capabilities, PRIMARY_POSITION);
                         assertEquals(PercentType.class, pos.getClass());
                         assertEquals(59, ((PercentType) pos).intValue());
 
-                        pos = shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN);
+                        pos = shadePos.getState(capabilities, SECONDARY_POSITION);
                         assertEquals(PercentType.class, pos.getClass());
                         assertEquals(35, ((PercentType) pos).intValue());
 
-                        pos = shadePos.getState(capabilities, VANE_TILT_COORDS);
+                        pos = shadePos.getState(capabilities, VANE_TILT_POSITION);
                         assertEquals(UnDefType.class, pos.getClass());
 
                         assertEquals(3, shadeData.batteryStatus);
@@ -442,18 +374,18 @@ public class HDPowerViewJUnitTests {
                         assertNotNull(shadePosition);
                         if (shadePosition != null) {
                             // ==== position2 ====
-                            State position2Old = shadePosition.getState(capabilities, SECONDARY_ZERO_IS_OPEN);
-                            shadePosition.setPosition(capabilities, PRIMARY_ZERO_IS_CLOSED, 99);
-                            State position2New = shadePosition.getState(capabilities, SECONDARY_ZERO_IS_OPEN);
+                            State position2Old = shadePosition.getState(capabilities, SECONDARY_POSITION);
+                            shadePosition.setPosition(capabilities, PRIMARY_POSITION, 99);
+                            State position2New = shadePosition.getState(capabilities, SECONDARY_POSITION);
                             assertEquals(PercentType.class, position2Old.getClass());
                             assertEquals(PercentType.class, position2New.getClass());
                             assertEquals(((PercentType) position2Old).intValue(),
                                     ((PercentType) position2New).intValue());
 
                             // ==== position2 ====
-                            State position1Old = shadePosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
-                            shadePosition.setPosition(capabilities, SECONDARY_ZERO_IS_OPEN, 99);
-                            State position1New = shadePosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED);
+                            State position1Old = shadePosition.getState(capabilities, PRIMARY_POSITION);
+                            shadePosition.setPosition(capabilities, SECONDARY_POSITION, 99);
+                            State position1New = shadePosition.getState(capabilities, PRIMARY_POSITION);
                             assertEquals(PercentType.class, position1Old.getClass());
                             assertEquals(PercentType.class, position1New.getClass());
                             assertEquals(((PercentType) position1Old).intValue(),
@@ -464,80 +396,4 @@ public class HDPowerViewJUnitTests {
             }
         }
     }
-
-    /**
-     * General tests of the database of known types.
-     */
-    @Test
-    public void testKnownTypesDatabase() {
-        assertTrue(db.isTypeInDatabase(4));
-        assertTrue(db.isCapabilitiesInDatabase(0));
-
-        assertTrue(db.getCapabilities(6).isPrimaryStateInverted());
-        assertTrue(db.getCapabilities(7).supportsSecondary());
-
-        assertEquals(db.getType(4).getCapabilities(), 0);
-        assertEquals(db.getType(-1).getCapabilities(), -1);
-
-        assertFalse(db.isTypeInDatabase(99));
-        assertFalse(db.isCapabilitiesInDatabase(99));
-
-        assertFalse(db.getCapabilities(0).isPrimaryStateInverted());
-        assertFalse(db.getCapabilities(-1).isPrimaryStateInverted());
-        assertFalse(db.getCapabilities(99).isPrimaryStateInverted());
-
-        assertFalse(db.getCapabilities(0).supportsSecondary());
-        assertFalse(db.getCapabilities(-1).supportsSecondary());
-        assertFalse(db.getCapabilities(99).supportsSecondary());
-    }
-
-    /**
-     * On dual rail shades, it should not be possible to drive the upper rail below the lower rail, or vice-versa. So
-     * the binding code applies constraints on setting such positions. This test checks that the constraint code is
-     * working.
-     */
-    @Test
-    public void testDualRailConstraints() {
-        ShadePosition shade = new ShadePosition();
-        Capabilities caps = db.getCapabilities(7);
-
-        // ==== OK !! primary at bottom, secondary at top ====
-        shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0);
-        assertEquals(PercentType.HUNDRED, shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-        assertEquals(PercentType.ZERO, shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-
-        // ==== OK !! primary at middle, secondary at top ====
-        shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 50).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0);
-        assertEquals(new PercentType(50), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-        assertEquals(PercentType.ZERO, shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-
-        // ==== OK !! primary at middle, secondary at middle ====
-        shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 50).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 50);
-        assertEquals(new PercentType(50), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-        assertEquals(new PercentType(50), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-
-        // ==== IMPOSSIBLE !! secondary at middle, primary above => test the constraining code ====
-        shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
-        shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 40).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 25);
-        assertEquals(new PercentType(40), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-        assertEquals(new PercentType(40), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-
-        // ==== OK !! secondary at middle, primary below ====
-        shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
-        shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 50).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 75);
-        assertEquals(new PercentType(50), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-        assertEquals(new PercentType(75), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-
-        // ==== IMPOSSIBLE !! primary at middle, secondary below => test the constraining code ====
-        shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
-        shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 60).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 75);
-        assertEquals(new PercentType(60), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-        assertEquals(new PercentType(60), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-
-        // ==== OK !! primary at middle, secondary above ====
-        shade.setPosition(caps, SECONDARY_ZERO_IS_OPEN, 0).setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 100);
-        shade.setPosition(caps, PRIMARY_ZERO_IS_CLOSED, 60).setPosition(caps, SECONDARY_ZERO_IS_OPEN, 25);
-        assertEquals(new PercentType(60), shade.getState(caps, PRIMARY_ZERO_IS_CLOSED));
-        assertEquals(new PercentType(25), shade.getState(caps, SECONDARY_ZERO_IS_OPEN));
-    }
 }
diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/ShadePositionTest.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/ShadePositionTest.java
new file mode 100644 (file)
index 0000000..818dc4a
--- /dev/null
@@ -0,0 +1,292 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.hdpowerview;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
+import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
+import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Unit tests for Shade Position setting and getting.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public class ShadePositionTest {
+
+    private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
+
+    /**
+     * General tests of the database of known types.
+     */
+    @Test
+    public void testKnownTypesDatabase() {
+        assertTrue(db.isTypeInDatabase(4));
+        assertTrue(db.isCapabilitiesInDatabase(0));
+
+        assertTrue(db.getCapabilities(0).supportsPrimary());
+        assertTrue(db.getCapabilities(0).supportsTiltOnClosed());
+        assertTrue(db.getCapabilities(1).supportsTiltAnywhere());
+        assertTrue(db.getCapabilities(2).supportsTilt180());
+        assertTrue(db.getCapabilities(3).supportsTiltOnClosed());
+        assertTrue(db.getCapabilities(4).supportsTilt180());
+        assertTrue(db.getCapabilities(5).supportsTilt180());
+        assertFalse(db.getCapabilities(5).supportsPrimary());
+        assertTrue(db.getCapabilities(6).isPrimaryStateInverted());
+        assertTrue(db.getCapabilities(7).supportsSecondary());
+        assertTrue(db.getCapabilities(8).supportsBlackoutShade());
+        assertTrue(db.getCapabilities(9).supportsBlackoutShade());
+
+        assertEquals(db.getType(4).getCapabilities(), 0);
+        assertEquals(db.getType(-1).getCapabilities(), -1);
+
+        assertFalse(db.isTypeInDatabase(99));
+        assertFalse(db.isCapabilitiesInDatabase(99));
+
+        assertFalse(db.getCapabilities(0).isPrimaryStateInverted());
+        assertFalse(db.getCapabilities(-1).isPrimaryStateInverted());
+        assertFalse(db.getCapabilities(99).isPrimaryStateInverted());
+
+        assertFalse(db.getCapabilities(0).supportsSecondary());
+        assertFalse(db.getCapabilities(-1).supportsSecondary());
+        assertFalse(db.getCapabilities(99).supportsSecondary());
+    }
+
+    /**
+     * Helper method; test if shade position is a PercentType and that its value is correct.
+     *
+     * @param position the shade position
+     * @param value the test value to compare with
+     */
+    private void assertShadePosition(State position, int value) {
+        assertEquals(PercentType.class, position.getClass());
+        assertEquals(value, ((PercentType) position).intValue());
+    }
+
+    /**
+     * Test parsing of ShadePosition (shade fully up).
+     *
+     */
+    @Test
+    public void testShadePositionParsingFullyUp() {
+        Capabilities capabilities = db.getCapabilities(0);
+        ShadePosition test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 0);
+        assertNotNull(test);
+        State pos = test.getState(capabilities, PRIMARY_POSITION);
+        assertShadePosition(pos, 0);
+        pos = test.getState(capabilities, VANE_TILT_POSITION);
+        assertTrue(UnDefType.UNDEF.equals(pos));
+    }
+
+    /**
+     * Test parsing of ShadePosition (shade fully down (method 1)).
+     *
+     */
+    @Test
+    public void testShadePositionParsingShadeFullyDown1() {
+        Capabilities capabilities = db.getCapabilities(0);
+        ShadePosition test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 100);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 0);
+    }
+
+    /**
+     * Test parsing of ShadePosition (shade fully down (method 2)).
+     *
+     */
+    @Test
+    public void testShadePositionParsingShadeFullyDown2() {
+        Capabilities capabilities = db.getCapabilities(0);
+        ShadePosition test = new ShadePosition().setPosition(capabilities, VANE_TILT_POSITION, 0);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 0);
+    }
+
+    /**
+     * Test parsing of ShadePosition (shade fully down (method 2) and vane fully open).
+     *
+     */
+    @Test
+    public void testShadePositionParsingShadeFullyDownVaneOpen() {
+        Capabilities capabilities = db.getCapabilities(0);
+        ShadePosition test = new ShadePosition().setPosition(capabilities, VANE_TILT_POSITION, 100);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 100);
+    }
+
+    /**
+     * On dual rail shades, it should not be possible to drive the upper rail below the lower rail, or vice-versa. So
+     * the binding code applies constraints on setting such positions. This test checks that the constraint code is
+     * working.
+     */
+    @Test
+    public void testDualRailConstraints() {
+        Capabilities capabilities = db.getCapabilities(7);
+        ShadePosition test = new ShadePosition();
+
+        // ==== OK !! primary at bottom, secondary at top ====
+        test.setPosition(capabilities, PRIMARY_POSITION, 100).setPosition(capabilities, SECONDARY_POSITION, 0);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // ==== OK !! primary at middle, secondary at top ====
+        test.setPosition(capabilities, PRIMARY_POSITION, 50).setPosition(capabilities, SECONDARY_POSITION, 0);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 50);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // ==== OK !! primary at middle, secondary at middle ====
+        test.setPosition(capabilities, PRIMARY_POSITION, 50).setPosition(capabilities, SECONDARY_POSITION, 50);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 50);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 50);
+
+        // ==== IMPOSSIBLE !! secondary at middle, primary above => test the constraining code ====
+        test.setPosition(capabilities, SECONDARY_POSITION, 0).setPosition(capabilities, PRIMARY_POSITION, 100);
+        test.setPosition(capabilities, SECONDARY_POSITION, 40).setPosition(capabilities, PRIMARY_POSITION, 25);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 40);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 40);
+
+        // ==== OK !! secondary at middle, primary below ====
+        test.setPosition(capabilities, SECONDARY_POSITION, 0).setPosition(capabilities, PRIMARY_POSITION, 100);
+        test.setPosition(capabilities, SECONDARY_POSITION, 50).setPosition(capabilities, PRIMARY_POSITION, 75);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 75);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 50);
+
+        // ==== IMPOSSIBLE !! primary at middle, secondary below => test the constraining code ====
+        test.setPosition(capabilities, SECONDARY_POSITION, 0).setPosition(capabilities, PRIMARY_POSITION, 100);
+        test.setPosition(capabilities, PRIMARY_POSITION, 60).setPosition(capabilities, SECONDARY_POSITION, 75);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 60);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 60);
+
+        // ==== OK !! primary at middle, secondary above ====
+        test.setPosition(capabilities, SECONDARY_POSITION, 0).setPosition(capabilities, PRIMARY_POSITION, 100);
+        test.setPosition(capabilities, PRIMARY_POSITION, 60).setPosition(capabilities, SECONDARY_POSITION, 25);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 60);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 25);
+    }
+
+    /**
+     * Test parsing of DuoLite shades having a secondary blackout shade.
+     *
+     */
+    @Test
+    public void testDuoliteShadePositionParsing() {
+        // blackout shades have capabilities 8
+        Capabilities capabilities = db.getCapabilities(8);
+        ShadePosition test;
+
+        // both shades up
+        test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 0);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 0);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // front shade 50% down
+        test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 50);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 50);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // front shade 100% down, back shade 0% down
+        test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 100);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // front shade 100% down, back shade 0% down (ALTERNATE)
+        test = new ShadePosition().setPosition(capabilities, SECONDARY_POSITION, 0);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // front shade 100% down, back shade 50% down
+        test = new ShadePosition().setPosition(capabilities, SECONDARY_POSITION, 50);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 50);
+
+        // front shade 100% down, back shade 100% down
+        test = new ShadePosition().setPosition(capabilities, SECONDARY_POSITION, 100);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 100);
+    }
+
+    /**
+     * Test parsing of DuoLite shades having both a secondary blackout shade, and tilt anywhere functionality.
+     *
+     */
+    @Test
+    public void testDuoliteTiltShadePositionParsing() {
+        // blackout shades with tilt have capabilities 9
+        Capabilities capabilities = db.getCapabilities(9);
+        ShadePosition test;
+
+        // both shades up, tilt 0%
+        test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 0).setPosition(capabilities,
+                VANE_TILT_POSITION, 0);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 0);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+
+        // front shade 50% down, tilt 30%
+        test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 50).setPosition(capabilities,
+                VANE_TILT_POSITION, 30);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 50);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 30);
+
+        // front shade 100% down, back shade 0% down, tilt 30%
+        test = new ShadePosition().setPosition(capabilities, PRIMARY_POSITION, 100).setPosition(capabilities,
+                VANE_TILT_POSITION, 30);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 30);
+
+        // front shade 100% down, back shade 0% down, tilt 30% (ALTERNATE)
+        test = new ShadePosition().setPosition(capabilities, SECONDARY_POSITION, 0).setPosition(capabilities,
+                VANE_TILT_POSITION, 30);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 0);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 30);
+
+        // front shade 100% down, back shade 50% down, tilt 30%
+        test = new ShadePosition().setPosition(capabilities, SECONDARY_POSITION, 50).setPosition(capabilities,
+                VANE_TILT_POSITION, 30);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 50);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 30);
+
+        // front shade 100% down, back shade 100% down, tilt 70%
+        test = new ShadePosition().setPosition(capabilities, SECONDARY_POSITION, 100).setPosition(capabilities,
+                VANE_TILT_POSITION, 70);
+        assertNotNull(test);
+        assertShadePosition(test.getState(capabilities, PRIMARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, SECONDARY_POSITION), 100);
+        assertShadePosition(test.getState(capabilities, VANE_TILT_POSITION), 70);
+    }
+}