From d45bcdb7aa78539c8b82305f7ab136554d1f77a5 Mon Sep 17 00:00:00 2001 From: Andrew Fiddian-Green Date: Tue, 4 Jan 2022 19:23:48 +0000 Subject: [PATCH] [hdpowerview] Fix secondary position bug. Add shade database and properties. (#11698) Signed-off-by: Andrew Fiddian-Green --- .../org.openhab.binding.hdpowerview/README.md | 65 ++- .../doc/left.png | Bin 0 -> 411 bytes .../doc/right.png | Bin 0 -> 728 bytes .../internal/HDPowerViewBindingConstants.java | 5 + .../internal/HDPowerViewWebTargets.java | 11 +- .../internal/api/ActuatorClass.java | 27 - .../internal/api/CoordinateSystem.java | 78 +-- .../internal/api/ShadePosition.java | 425 ++++++++++------ .../internal/api/responses/Shades.java | 1 + .../database/ShadeCapabilitiesDatabase.java | 339 +++++++++++++ .../HDPowerViewShadeDiscoveryService.java | 20 +- .../handler/HDPowerViewShadeHandler.java | 194 ++++++-- .../resources/OH-INF/thing/thing-types.xml | 4 + .../hdpowerview/HDPowerViewJUnitTests.java | 468 ++++++++++++------ 14 files changed, 1193 insertions(+), 444 deletions(-) create mode 100644 bundles/org.openhab.binding.hdpowerview/doc/left.png create mode 100644 bundles/org.openhab.binding.hdpowerview/doc/right.png delete mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ActuatorClass.java create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/database/ShadeCapabilitiesDatabase.java diff --git a/bundles/org.openhab.binding.hdpowerview/README.md b/bundles/org.openhab.binding.hdpowerview/README.md index df43456128b..1c70a3a68f6 100644 --- a/bundles/org.openhab.binding.hdpowerview/README.md +++ b/bundles/org.openhab.binding.hdpowerview/README.md @@ -13,10 +13,10 @@ By using a scene to control multiple shades at once, the shades will all begin m ## Supported Things -| Thing | Thing Type | Description | -|-----------------|------------|--------------------| -| PowerView Hub | Bridge | The PowerView hub provides the interface between your network and the shade's radio network. It also contains channels used to interact with scenes. | -| PowerView Shade | Thing | A motorized shade. | +| Thing | Thing Type | Description | +|-------|------------|-------------| +| hub | Bridge | The PowerView hub provides the interface between your network and the shade's radio network. It also contains channels used to interact with scenes. | +| shade | Thing | A motorized shade. | ## Discovery @@ -52,13 +52,13 @@ PowerView shades should preferably be configured via the automatic discovery pro It is quite difficult to configure manually as the `id` of the shade is not exposed in the PowerView app. However, the configuration parameters are described below: -| Configuration Parameter | Description | -|-------------------------|---------------------------------------------------------------| +| Configuration Parameter | Description | +|-------------------------|-------------| | id | The ID of the PowerView shade in the app. Must be an integer. | ## Channels -### Channels for PowerView Hub +### Channels for Hub (Thing type `hub`) Scene, scene group and automation channels will be added dynamically to the binding as they are discovered in the hub. Each will have an entry in the hub as shown below, whereby different scenes, scene groups and automations @@ -70,7 +70,7 @@ have different `id` values: | sceneGroups | id | Switch | Setting this to ON will activate the scene group. Scene groups are stateless in the PowerView hub; they have no on/off state. Note: include `{autoupdate="false"}` in the item configuration to avoid having to reset it to off after use. | | automations | id | Switch | Setting this to ON will enable the automation, while OFF will disable it. | -### Channels for PowerView Shade +### Channels for Shades (Thing type `shade`) A shade always implements a roller shutter channel `position` which controls the vertical position of the shade's (primary) rail. If the shade has slats or rotatable vanes, there is also a dimmer channel `vane` which controls the slat / vane position. @@ -80,7 +80,7 @@ All of these channels appear in the binding, but only those which have a physica | Channel | Item Type | Description | |----------------|--------------------------|-------------| | position | Rollershutter | The vertical position of the shade's rail -- see [next chapter](#Roller-Shutter-Up/Down-Position-vs.-Open/Close-State). Up/Down commands will move the rail completely up or completely down. Percentage commands will move the rail to an intermediate position. Stop commands will halt any current movement of the rail. | -| secondary | Rollershutter | The vertical position of the secondary rail (if any). Its function is basically identical to the `position` channel above -- but see [next chapter](#Roller-Shutter-Up/Down-Position-vs.-Open/Close-State). | +| secondary | Rollershutter | The vertical position of the secondary rail (if any). Its function is similar to the `position` channel above -- but see [next chapter](#Roller-Shutter-Up/Down-Position-vs.-Open/Close-State). | | vane | Dimmer | The degree of opening of the slats or vanes. Setting this to a non-zero value will first move the shade `position` fully down, since the slats or vanes can only have a defined state if the shade is in its down position -- see [Interdependency between Channel positions](#Interdependency-between-Channel-positions). | | lowBattery | Switch | Indicates ON when the battery level of the shade is low, as determined by the hub's internal rules. | | batteryLevel | Number | Battery level (10% = low, 50% = medium, 100% = high) @@ -95,20 +95,34 @@ And for horizontal shades, it maps the horizontal position of the "truck" to the 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.. -| Type of Shade | Channel | Rollershutter Command | Motion direction | Shade State | Percent | -|--------------------------|-------------------|-----------------------|------------------|----------------|---------| -| Single action bottom-up | `position` | ▲ | Up | `OPEN` | 0% | -| | | ▼ | Down | `CLOSED` | 100% | -| Single action top-down | `position` | ▲ | Up | ***`CLOSED`*** | 0% | -| | | ▼ | Down | ***`OPEN`*** | 100% | -| Single action right-left | `position` | ▲ | ***Left*** | `OPEN` | 0% | -| | | ▼ | ***Right*** | `CLOSED` | 100% | -| Single action left-right | `position` | ▲ | ***Right*** | `OPEN` | 0% | -| | | ▼ | ***Left*** | `CLOSED` | 100% | -| Dual action (lower rail) | `position` | ▲ | Up | `OPEN` | 0% | -| | | ▼ | Down | `CLOSED` | 100% | -| Dual action (upper rail) | ***`secondary`*** | ▲ | ***Down*** | `OPEN` | 0% | -| | | ▼ | ***Up*** | `CLOSED` | 100% | +| Type of Shade | Channel | Rollershutter Command | Motion direction | Shade State | Percent | Pebble Remote Button | +|-----------------------------|-------------------|-----------------------|------------------|----------------|-------------------|----------------------| +| Single action
bottom-up | `position` | ▲ | Up | `OPEN` | 0% | ▲ | +| | | ▼ | Down | `CLOSED` | 100% | ▼ | +| Single action
top-down | `position` | ▲ | Up | ***`CLOSED`*** | 0% | ▲ | +| | | ▼ | Down | ***`OPEN`*** | 100% | ▼ | +| Single action
right-left | `position` | ▲ | ***Left*** | `OPEN` | 0% | ▲ | +| | | ▼ | ***Right*** | `CLOSED` | 100% | ▼ | +| Single action
left-right | `position` | ▲ | ***Right*** | `OPEN` | 0% | ▲ | +| | | ▼ | ***Left*** | `CLOSED` | 100% | ▼ | +| Dual action
(lower rail) | `position` | ▲ | Up | `OPEN` | 0% | ▲ | +| | | ▼ | Down | `CLOSED` | 100% | ▼ | +| Dual action
(upper rail) | ***`secondary`*** | ▲ | Up | ***`CLOSED`*** | 0%1) | ![](doc/right.png) | +| | | ▼ | Down | ***`OPEN`*** | 100%1) | ![](doc/left.png) | + +***1) 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. +After moving the shade, the percent value would initially display the correct value, but on the next refresh it would 'flip' to the **inverse** of the correct value. +The details are shown in the following table. +This bug has been fixed from openHAB v3.2.x (or later) — +***so if you have rules that depend on the percent value, and you update from an earlier openHAB version to v3.2.x (or later), you will need to modify them!*** + +| Channel | UI Control Element | UI Control Command | Immediate Action
on Shade State | Dimmer Percent Display
(Initial => Final) | +|-------------|--------------------|---------------------|------------------------------------|----------------------------------------------| +| `secondary` | RollerShutter | Press `UP` button | Rail moves Up (`CLOSED`) | 0% (initial) => 100% (final) | +| | | Press `DOWN` button | Rail moves Down (`OPEN`) | 100% (initial) => 0% (final) | +| | Dimmer | Move slider to 0% | Rail moves Up (`CLOSED`) | 0% (initial) => 100% (final) | +| | | Move slider to 100% | Rail moves Down (`OPEN`) | 100% (initial) => 0% (final) | ### Interdependency between Channel positions @@ -123,8 +137,9 @@ So there is an interdependency between the value of `vane` and the value of `pos | Shade 100% down, Vane 50% | 100% = `DOWN` | 50% | | Shade 100% down, Vane 100% | 100% = `DOWN` | 100% | -On dual action shades, the top rail cannot move below the position of the bottom rail. -So the value of `secondary` may be constrained by the value of `position`. +On dual action shades, the top rail cannot move below the bottom rail, nor can the bottom rail move above the top. +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`. ## Refreshing the PowerView Hub Cache diff --git a/bundles/org.openhab.binding.hdpowerview/doc/left.png b/bundles/org.openhab.binding.hdpowerview/doc/left.png new file mode 100644 index 0000000000000000000000000000000000000000..d0c0a1d899e4cb088564abf74558c2645b34b12c GIT binary patch literal 411 zcmX|%TPQ%Sdm?1-JyaS$n&$USzBA(HGtA(yl@?kMG3Jfu>cZS8zTTn1CSgO3E=xSkPAR1 z3q>^6&r)9jL^WGVEr4e_E>y1u;N(k0D*&3N0qFI5lgY$!oGi-#2qA(XD4U`vp63CW z&1R0{9Dfi(04QGR|C>&yBZMeYk|ZE(D2h^~BuSc|sX_o5J%YUqR8AP9(P(5@Rusir z*}1s5y6W|M{eHjy;(qgRyBvU~+g2q5T3qqzFUV!I9*COU(=*P8c(=fbyHXL;vI&FOpk VHzMAmpFZ8Z3IJHEomr>rT))F?hfn|j literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.hdpowerview/doc/right.png b/bundles/org.openhab.binding.hdpowerview/doc/right.png new file mode 100644 index 0000000000000000000000000000000000000000..794787c5e4bd0cfb3ee809c6fe530867d94bf705 GIT binary patch literal 728 zcmYk2QAkq(7=_Pu8_nr5hO9Y^YqD~YZV!=jBjh?Jb7OZ6-OV^1IST>AVle<5$1w~O zi^V94;y7+$O-cEXxiI3;nQ^#TZmLI4Z~1IKYXosMPM zI0hgP2mr{<&1D$I@Aqpo8k(j7uq><7=>W*(a=YCQpsA^;tgNi2rpD!RZQI>$cl@rd zu8#9!u^2#BRu%x6Og26~4#4B_08lEG6h*~#k|eEGYuxMe`SSDgwOXx4quEXdkeQjO z)oOEca{d=dlB8*xAPABql}aT5kw_$!N>wTq&+~mYK{J4g#$tO3h=c@zKqwT7L_5Ud z#6*cCDOsA5lCn#dDp#gw>{Y1`W@`@}DYDV@QAY_=>T;hfuc$obJzZOSuDQ825bWne z*N1LRPR~4@d-nXr>o*IF@7^y(S60_PZ+!dyV{`M*M0Uej0ErDnE+4S-;not<^}}0b zCoQG|r@EvvtY2aIFU9s?EWON8unl6-6$yR zCipyUTW?)sa0pK9DVl#|a=-0Kz4KtOWLAG*UdUgiGDg1e(c8((VZu&#-Ebt8t5c-n uzMn$9+Nf7hC(P!S#M#HNg+h7DD*}ZAuV&Kf+m4yjwq9>3w3|kaZ0ldPO57p< literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java index e4352e6a7b0..dc056626ebd 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java @@ -54,6 +54,11 @@ public class HDPowerViewBindingConstants { public static final String CHANNELTYPE_SCENE_GROUP_ACTIVATE = "scene-group-activate"; public static final String CHANNELTYPE_AUTOMATION_ENABLED = "automation-enabled"; + public static final String PROPERTY_SHADE_TYPE = "type"; + public static final String PROPERTY_SHADE_CAPABILITIES = "capabilities"; + public static final String PROPERTY_SECONDARY_RAIL_DETECTED = "secondaryRailDetected"; + public static final String PROPERTY_TILT_ANYWHERE_DETECTED = "tiltAnywhereDetected"; + public static final List NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub"); public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>(); diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java index c890c4ec45e..b2b75b3a1a0 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java @@ -97,6 +97,11 @@ public class HDPowerViewWebTargets { public String getValue() { return value; } + + @Override + public String toString() { + return String.format("?%s=%s", key, value); + } } /** @@ -247,7 +252,11 @@ public class HDPowerViewWebTargets { private synchronized String invoke(HttpMethod method, String url, @Nullable Query query, @Nullable String jsonCommand) throws HubMaintenanceException, HubProcessingException { if (logger.isTraceEnabled()) { - logger.trace("API command {} {}", method, url); + if (query != null) { + logger.trace("API command {} {}{}", method, url, query); + } else { + logger.trace("API command {} {}", method, url); + } if (jsonCommand != null) { logger.trace("JSON command = {}", jsonCommand); } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ActuatorClass.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ActuatorClass.java deleted file mode 100644 index 0fe4275954f..00000000000 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ActuatorClass.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.internal.api; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * Actuator class; all shades have a PRIMARY class actuator, plus double action - * shades also have a SECONDARY class actuator - * - * @author Andrew Fiddian-Green - Initial contribution - */ -@NonNullByDefault -public enum ActuatorClass { - PRIMARY_ACTUATOR, - SECONDARY_ACTUATOR; -} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java index c9e90e34721..f7e55a10933 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java @@ -13,18 +13,20 @@ package org.openhab.binding.hdpowerview.internal.api; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** - * Shade coordinate system, as returned by the HD PowerView hub - * - * @param ZERO_IS_CLOSED coordinate value 0 means shade is closed - * @param ZERO_IS_OPEN coordinate value 0 means shade is open - * @param VANE_COORDS coordinate system for vanes - * @param ERROR_UNKNOWN unsupported coordinate system + * 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 ERROR_UNKNOWN unsupported coordinate system. * * @author Andy Lintner - Initial contribution of the original enum called * ShadePositionKind - * + * * @author Andrew Fiddian-Green - Rewritten as a new enum called * CoordinateSystem to support secondary rail positions and be more * explicit on coordinate directions and ranges @@ -41,71 +43,69 @@ public enum CoordinateSystem { * like the top-down and the bottom-up, it operates in two where the top * (middle) rail closed value is 0 and the bottom (primary) rail closed position * is also 0 and fully open for both is 65535 - * + * * The position element can take on multiple states depending on the family of * shade under control. * - * The ranges of position integer values are + * The ranges of position integer values are * shades: 0..65535 * vanes: 0..32767 - * - * Shade fully up: (top-down: open, bottom-up: closed) - * posKind: 1 {ZERO_IS_CLOSED} + * + * Shade fully up: (top-down: open, bottom-up: closed) + * posKind: 1 {ZERO_IS_CLOSED} * position: 65535 * - * Shade and vane fully down: (top-down: closed, bottom-up: open) + * Shade and vane fully down: (top-down: closed, bottom-up: open) * posKind: 1 {ZERO_IS_CLOSED} * position1: 0 - * + * * ALTERNATE: Shade and vane fully down: (top-down: closed, bottom-up: open) * posKind: 3 {VANE_COORDS} * position: 0 * - * Shade fully down (closed) and vane fully up (open): + * Shade fully down (closed) and vane fully up (open): * posKind: 3 {VANE_COORDS} * position: 32767 * - * Dual action, secondary top-down shade fully up (closed): + * Dual action, secondary top-down shade fully up (closed): * posKind: 2 {ZERO_IS_OPEN} * position: 0 - * - * Dual action, secondary top-down shade fully down (open): + * + * Dual action, secondary top-down shade fully down (open): * posKind: 2 {ZERO_IS_OPEN} * position: 65535 - * + * */ - ZERO_IS_CLOSED, - ZERO_IS_OPEN, - VANE_COORDS, + NONE, + PRIMARY_ZERO_IS_CLOSED, + SECONDARY_ZERO_IS_OPEN, + VANE_TILT_COORDS, ERROR_UNKNOWN; public static final int MAX_SHADE = 65535; public static final int MAX_VANE = 32767; /** - * Converts an HD PowerView posKind integer value to a CoordinateSystem enum value - * - * @param posKind input integer value - * @return corresponding CoordinateSystem enum + * Converts an HD PowerView posKind integer value to a CoordinateSystem enum value. + * + * @param posKind input integer value. + * @return corresponding CoordinateSystem enum. */ public static CoordinateSystem fromPosKind(int posKind) { - switch (posKind) { - case 1: - return ZERO_IS_CLOSED; - case 2: - return ZERO_IS_OPEN; - case 3: - return VANE_COORDS; + try { + return CoordinateSystem.values()[posKind]; + } catch (ArrayIndexOutOfBoundsException e) { + return ERROR_UNKNOWN; } - return ERROR_UNKNOWN; } /** - * Converts a CoordinateSystem enum to an HD PowerView posKind integer value - * - * @return the posKind integer value + * Check if the coordinate system matches the given posKind. + * + * @param posKind + * @return true if equal. */ - public int toPosKind() { - return ordinal() + 1; + public boolean equals(@Nullable Integer posKind) { + return (posKind != null) && (posKind.intValue() == ordinal()); } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java index c7e5f0477d9..c9df727b6cf 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java @@ -16,9 +16,12 @@ import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The position of a single shade, as returned by the HD PowerView hub @@ -28,159 +31,126 @@ import org.openhab.core.types.UnDefType; */ @NonNullByDefault public class ShadePosition { + + private final transient Logger logger = LoggerFactory.getLogger(ShadePosition.class); + /** - * Primary actuator position + * Primary actuator position. */ private int posKind1; private int position1; /** - * Secondary actuator position + * Secondary actuator position. * - * here we have to use Integer objects rather than just int primitives because - * these are secondary optional position elements in the JSON payload, so the - * GSON de-serializer might leave them as null + * Here we have to use Integer objects rather than just int primitives because these are secondary optional position + * elements in the JSON payload, so the GSON de-serializer might leave them as null. */ private @Nullable Integer posKind2 = null; private @Nullable Integer position2 = null; - /** - * Create a ShadePosition position instance with just a primary actuator - * position - * - * @param coordSys the Coordinate System to be used - * @param percent the percentage position within that Coordinate System - * @return the ShadePosition instance - */ - public static ShadePosition create(CoordinateSystem coordSys, int percent) { - return new ShadePosition(coordSys, percent, null, null); + public ShadePosition() { } /** - * Create a ShadePosition position instance with both a primary and a secondary - * actuator position + * Get the shade's State for the given actuator class resp. coordinate system. * - * @param primaryCoordSys the Coordinate System to be used for the primary - * position - * @param primaryPercent the percentage position for primary position - * @param secondaryCoordSys the Coordinate System to be used for the secondary - * position - * @param secondaryPercent the percentage position for secondary position - * @return the ShadePosition instance + * @param shadeCapabilities the shade Thing capabilities. + * @param posKindCoords the actuator class (coordinate system) whose state is to be returned. + * @return the current state. */ - public static ShadePosition create(CoordinateSystem primaryCoordSys, int primaryPercent, - @Nullable CoordinateSystem secondaryCoordSys, @Nullable Integer secondaryPercent) { - return new ShadePosition(primaryCoordSys, primaryPercent, secondaryCoordSys, secondaryPercent); - } - - /** - * Constructor for ShadePosition position with both a primary and a secondary - * actuator position - * - * @param primaryCoordSys the Coordinate System to be used for the primary - * position - * @param primaryPercent the percentage position for primary position - * @param secondaryCoordSys the Coordinate System to be used for the secondary - * position - * @param secondaryPercent the percentage position for secondary position - */ - ShadePosition(CoordinateSystem primaryCoordSys, int primaryPercent, @Nullable CoordinateSystem secondaryCoordSys, - @Nullable Integer secondaryPercent) { - setPosition1(primaryCoordSys, primaryPercent); - setPosition2(secondaryCoordSys, secondaryPercent); - } - - /** - * For a given Actuator Class and Coordinate System, map the ShadePosition's - * state to an OpenHAB State - * - * @param actuatorClass the requested Actuator Class - * @param coordSys the requested Coordinate System - * @return the corresponding OpenHAB State - */ - public State getState(ActuatorClass actuatorClass, CoordinateSystem coordSys) { - switch (actuatorClass) { - case PRIMARY_ACTUATOR: - return getPosition1(coordSys); - case SECONDARY_ACTUATOR: - return getPosition2(coordSys); - default: - return UnDefType.UNDEF; + public State getState(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) { + State result = getPosition1(shadeCapabilities, posKindCoords); + if (result == UnDefType.UNDEF) { + result = getPosition2(shadeCapabilities, posKindCoords); } + logger.trace("getState(): capabilities={}, coords={} => result={}", shadeCapabilities, posKindCoords, result); + return result; } /** - * Determine the Coordinate System used for the given Actuator Class (if any) + * Set the shade's position1 value for the given actuator class resp. coordinate system. * - * @param actuatorClass the requested Actuator Class - * @return the Coordinate System used for that Actuator Class, or ERROR_UNKNOWN - * if the Actuator Class is not implemented + * @param shadeCapabilities the shade Thing capabilities. + * @param posKindCoords the actuator class (coordinate system) whose state is to be changed. + * @param percent the new position value. */ - public CoordinateSystem getCoordinateSystem(ActuatorClass actuatorClass) { - switch (actuatorClass) { - case PRIMARY_ACTUATOR: - return fromPosKind(posKind1); - case SECONDARY_ACTUATOR: - Integer posKind2 = this.posKind2; - if (posKind2 != null) { - return fromPosKind(posKind2.intValue()); - } - default: - return ERROR_UNKNOWN; - } - } - - private void setPosition1(CoordinateSystem coordSys, int percent) { - posKind1 = coordSys.toPosKind(); - switch (coordSys) { - case ZERO_IS_CLOSED: - /*- - * Primary rail of a single action bottom-up shade, or - * Primary, lower, bottom-up, rail of a dual action shade - */ - case ZERO_IS_OPEN: - /*- - * Primary rail of a single action top-down shade - * - * All these types use the same coordinate system; which is inverted in relation - * to that of OpenHAB - */ - position1 = MAX_SHADE - (int) Math.round(percent / 100d * MAX_SHADE); - break; - case VANE_COORDS: + private void setPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) { + switch (posKindCoords) { + case PRIMARY_ZERO_IS_CLOSED: /* - * Vane angle of the primary rail of a bottom-up single action shade + * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED */ - position1 = (int) Math.round(percent / 100d * MAX_VANE); + 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); + if (secondary instanceof PercentType) { + int secPercent = ((PercentType) secondary).intValue(); + if (percent < secPercent) { + percent = secPercent; + } + } + } + posKind1 = posKindCoords.ordinal(); + position1 = MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE); break; + + case SECONDARY_ZERO_IS_OPEN: + /* + * Secondary, upper rail of a dual action shade: => NOT INVERTED + */ + posKind1 = posKindCoords.ordinal(); + position1 = (int) Math.round((double) percent / 100 * MAX_SHADE); + break; + + case VANE_TILT_COORDS: + /* + * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED + */ + posKind1 = posKindCoords.ordinal(); + int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE; + position1 = (int) Math.round((double) percent / 100 * max); + break; + default: + posKind1 = CoordinateSystem.NONE.ordinal(); position1 = 0; } } - private State getPosition1(CoordinateSystem coordSys) { - switch (coordSys) { - case ZERO_IS_CLOSED: - /*- - * Primary rail of a single action bottom-up shade, or - * Primary, lower, bottom-up, rail of a dual action shade - */ - case ZERO_IS_OPEN: + /** + * Get the shade's position1 State for the given actuator class resp. coordinate system. + * + * @param shadeCapabilities the shade Thing capabilities. + * @param posKindCoords the actuator class (coordinate system) whose state is to be returned. + * @return the State (or UNDEF if not available). + */ + private State getPosition1(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) { + switch (posKindCoords) { + case PRIMARY_ZERO_IS_CLOSED: /* - * Primary rail of a single action top-down shade - * - * All these types use the same coordinate system; which is inverted in relation - * to that of OpenHAB - * - * If the slats have a defined position then the shade position must by - * definition be 100% + * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED */ - return posKind1 == 3 ? PercentType.HUNDRED - : new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100)); + if (posKindCoords.equals(posKind1)) { + return new PercentType(100 - (int) Math.round((double) position1 / MAX_SHADE * 100)); + } + if (VANE_TILT_COORDS.equals(posKind1) && shadeCapabilities.supportsTiltOnClosed()) { + return PercentType.HUNDRED; + } + break; - case VANE_COORDS: + case SECONDARY_ZERO_IS_OPEN: /* - * Vane angle of the primary rail of a bottom-up single action shade + * Secondary, upper rail of a dual action shade: => NOT INVERTED + */ + if (posKindCoords.equals(posKind1)) { + return new PercentType((int) Math.round((double) position1 / MAX_SHADE * 100)); + } + break; + + case VANE_TILT_COORDS: + /* + * Vane angle of the primary rail of a bottom-up single action shade: => NOT INVERTED * * If the shades are not open, the vane position is undefined; if the the shades * are exactly open then the vanes are at zero; otherwise return the actual vane @@ -190,56 +160,205 @@ public class ShadePosition { * be a bug in the hub) so we avoid an out of range exception via the Math.min() * function below.. */ - return posKind1 != 3 ? (position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO) - : new PercentType((int) Math.round((double) Math.min(position1, MAX_VANE) / MAX_VANE * 100)); - - default: - return UnDefType.UNDEF; - } - } - - private void setPosition2(@Nullable CoordinateSystem coordSys, @Nullable Integer percent) { - if (coordSys == null || percent == null) { - return; - } - posKind2 = Integer.valueOf(coordSys.toPosKind()); - switch (coordSys) { - case ZERO_IS_CLOSED: - case ZERO_IS_OPEN: - /* - * Secondary, upper, top-down rail of a dual action shade - * - * Uses a coordinate system that is NOT inverted in relation to OpenHAB - */ - position2 = Integer.valueOf((int) Math.round(percent.doubleValue() / 100 * MAX_SHADE)); + if (posKindCoords.equals(posKind1)) { + 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()) { + return position1 != 0 ? UnDefType.UNDEF : PercentType.ZERO; + } break; + + case ERROR_UNKNOWN: + case NONE: + // fall through, return UNDEF + } + return UnDefType.UNDEF; + } + + /** + * Set the shade's position2 value for the given actuator class resp. coordinate system. + * + * @param shadeCapabilities the shade Thing capabilities. + * @param posKindCoords the actuator class (coordinate system) whose state is to be changed. + * @param percent the new position value. + */ + private void setPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) { + switch (posKindCoords) { + case PRIMARY_ZERO_IS_CLOSED: + /* + * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED + */ + posKind2 = posKindCoords.ordinal(); + position2 = Integer.valueOf(MAX_SHADE - (int) Math.round((double) percent / 100 * MAX_SHADE)); + break; + + case SECONDARY_ZERO_IS_OPEN: + /* + * 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); + if (primary instanceof PercentType) { + int primaryPercent = ((PercentType) primary).intValue(); + if (percent > primaryPercent) { + percent = primaryPercent; + } + } + } + posKind2 = posKindCoords.ordinal(); + position2 = Integer.valueOf((int) Math.round((double) percent / 100 * MAX_SHADE)); + break; + + case VANE_TILT_COORDS: + posKind2 = posKindCoords.ordinal(); + int max = shadeCapabilities.supportsTilt180() ? MAX_SHADE : MAX_VANE; + position2 = Integer.valueOf((int) Math.round((double) percent / 100 * max)); + break; + default: - position2 = Integer.valueOf(0); + posKind2 = null; + position2 = null; } } - private State getPosition2(CoordinateSystem coordSys) { + /** + * Get the shade's position2 State for the given actuator class resp. coordinate system. + * + * @param shadeCapabilities the shade Thing capabilities. + * @param posKindCoords the actuator class (coordinate system) whose state is to be returned. + * @return the State (or UNDEF if not available). + */ + private State getPosition2(Capabilities shadeCapabilities, CoordinateSystem posKindCoords) { Integer posKind2 = this.posKind2; Integer position2 = this.position2; + if (position2 == null || posKind2 == null) { return UnDefType.UNDEF; } - switch (coordSys) { - case ZERO_IS_CLOSED: + + switch (posKindCoords) { + case PRIMARY_ZERO_IS_CLOSED: /* - * This case should never occur; but return a value anyway just in case + * Primary rail of a bottom-up shade, or lower rail of a dual action shade: => INVERTED */ - case ZERO_IS_OPEN: + if (posKindCoords.equals(posKind2)) { + return new PercentType(100 - (int) Math.round(position2.doubleValue() / MAX_SHADE * 100)); + } + break; + + case SECONDARY_ZERO_IS_OPEN: /* - * Secondary, upper, top-down rail of a dual action shade - * - * Uses a coordinate system that is NOT inverted in relation to OpenHAB + * Secondary, upper rail of a dual action shade: => NOT INVERTED */ - if (posKind2.intValue() != 3) { + if (posKindCoords.equals(posKind2)) { return new PercentType((int) Math.round(position2.doubleValue() / MAX_SHADE * 100)); } - default: - return UnDefType.UNDEF; + break; + + /* + * 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: + 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)); + } + break; + + case ERROR_UNKNOWN: + case NONE: + // fall through, return UNDEF } + return UnDefType.UNDEF; + } + + /** + * Detect if the ShadePosition has a posKindN value indicating potential support for a secondary rail. + * + * @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); + } + + /** + * Detect if the ShadePosition has both a posKindN value indicating potential support for tilt, AND a posKindN + * indicating support for a primary rail. i.e. it potentially supports tilt anywhere functionality. + * + * @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))))); + } + + /** + * Set the shade's position for the given actuator class resp. coordinate system. + * + * @param shadeCapabilities the shade Thing capabilities. + * @param posKindCoords the actuator class (coordinate system) whose state is to be changed. + * @param percent the new position value. + * @return this object. + */ + public ShadePosition setPosition(Capabilities shadeCapabilities, CoordinateSystem posKindCoords, int percent) { + 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)) { + final Integer posKind2Temp = posKind2; + final Integer position2Temp = position2; + posKind2 = Integer.valueOf(posKind1); + position2 = Integer.valueOf(position1); + posKind1 = posKind2Temp != null ? posKind2Temp.intValue() : NONE.ordinal(); + position1 = position2Temp != null ? position2Temp.intValue() : 0; + } + + // delete position2 if it has an invalid position kind + if (ERROR_UNKNOWN.equals(posKind2) || NONE.equals(posKind2)) { + posKind2 = null; + position2 = null; + } + + // logic to set either position1 or position2 + switch (posKindCoords) { + case PRIMARY_ZERO_IS_CLOSED: + if (shadeCapabilities.supportsPrimary()) { + setPosition1(shadeCapabilities, posKindCoords, percent); + } + break; + + case SECONDARY_ZERO_IS_OPEN: + if (shadeCapabilities.supportsSecondary()) { + if (shadeCapabilities.supportsPrimary()) { + setPosition2(shadeCapabilities, posKindCoords, percent); + } else { + setPosition1(shadeCapabilities, posKindCoords, percent); + } + } + break; + + case VANE_TILT_COORDS: + if (shadeCapabilities.supportsPrimary()) { + if (shadeCapabilities.supportsTiltOnClosed()) { + setPosition1(shadeCapabilities, posKindCoords, percent); + } else if (shadeCapabilities.supportsTiltAnywhere()) { + setPosition2(shadeCapabilities, posKindCoords, percent); + } + } else if (shadeCapabilities.supportsTiltAnywhere()) { + setPosition1(shadeCapabilities, posKindCoords, percent); + } + break; + + case ERROR_UNKNOWN: + case NONE: + // fall through, do nothing + } + return this; } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java index 3a4a9eef777..c86d965b814 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java @@ -51,6 +51,7 @@ public class Shades { public @Nullable ShadePosition positions; public @Nullable Boolean timedOut; public int signalStrength; + public @Nullable Integer capabilities; public String getName() { return new String(Base64.getDecoder().decode(name)); diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/database/ShadeCapabilitiesDatabase.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/database/ShadeCapabilitiesDatabase.java new file mode 100644 index 00000000000..87c58b0dd19 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/database/ShadeCapabilitiesDatabase.java @@ -0,0 +1,339 @@ +/** + * Copyright (c) 2010-2021 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.internal.database; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class containing the database of all known shade 'types' and their respective 'capabilities'. + * + * If user systems detect shade types that are not in the database, then this class can issue logger warning messages + * indicating such absence, and prompting the user to report it to developers so that the database and the respective + * binding functionality can (hopefully) be extended over time. + * + * @author Andrew Fiddian-Green - Initial Contribution + */ +@NonNullByDefault +public class ShadeCapabilitiesDatabase { + + private final Logger logger = LoggerFactory.getLogger(ShadeCapabilitiesDatabase.class); + + /* + * Database of known shade capabilities. + */ + private static final Map CAPABILITIES_DATABASE = Arrays.asList( + // @formatter:off + new Capabilities(0).primary().tiltOnClosed() .text("Bottom Up"), + new Capabilities(1).primary().tiltAnywhere() .text("Bottom Up Tilt 90°"), + new Capabilities(2).primary().tiltAnywhere().tilt180() .text("Bottom Up Tilt 180°"), + new Capabilities(3).primary().tiltOnClosed() .text("Vertical"), + new Capabilities(4).primary().tiltAnywhere().tilt180() .text("Vertical Tilt 180°"), + 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°"), + // @formatter:on + new Capabilities()).stream().collect(Collectors.toMap(Capabilities::getValue, Function.identity())); + + /* + * Database of known shade types and corresponding capabilities. + */ + private static final Map TYPE_DATABASE = Arrays.asList( + // @formatter:off + new Type( 4).capabilities(0).text("Roman"), + new Type( 5).capabilities(0).text("Bottom Up"), + new Type( 6).capabilities(0).text("Duette"), + new Type( 7).capabilities(6).text("Top Down"), + new Type( 8).capabilities(7).text("Duette Top Down Bottom Up"), + new Type( 9).capabilities(7).text("Duette DuoLite Top Down Bottom Up"), + new Type(23).capabilities(1).text("Silhouette"), + new Type(42).capabilities(0).text("M25T Roller Blind"), + new Type(43).capabilities(1).text("Facette"), + new Type(44).capabilities(0).text("Twist"), + new Type(47).capabilities(7).text("Pleated Top Down Bottom Up"), + new Type(49).capabilities(0).text("AC Roller"), + new Type(51).capabilities(2).text("Venetian"), + new Type(54).capabilities(3).text("Vertical Slats Left Stack"), + new Type(55).capabilities(3).text("Vertical Slats Right Stack"), + new Type(56).capabilities(3).text("Vertical Slats Split Stack"), + new Type(62).capabilities(2).text("Venetian"), + new Type(69).capabilities(3).text("Curtain Left Stack"), + new Type(70).capabilities(3).text("Curtain Right Stack"), + new Type(71).capabilities(3).text("Curtain Split Stack"), + new Type(79).capabilities(8).text("Duolite Lift"), + // @formatter:on + new Type()).stream().collect(Collectors.toMap(Type::getValue, Function.identity())); + + /** + * Base class that is extended by Type and Capabilities classes. + * + * @author Andrew Fiddian-Green - Initial Contribution + */ + private static class Base { + protected int intValue = -1; + protected String text = "-- not in database --"; + + protected Integer getValue() { + return intValue; + } + + @Override + public String toString() { + return String.format("%s (%d)", text, intValue); + } + } + + /** + * Describes a shade type entry in the database; implements 'capabilities' parameter. + * + * @author Andrew Fiddian-Green - Initial Contribution + */ + public static class Type extends Base { + private int capabilities = -1; + + protected Type() { + } + + protected Type(int type) { + intValue = type; + } + + protected Type text(String text) { + this.text = text; + return this; + } + + protected Type capabilities(int capabilities) { + this.capabilities = capabilities; + return this; + } + + /** + * Get shade types's 'capabilities'. + * + * @return 'capabilities'. + */ + public int getCapabilities() { + return capabilities; + } + } + + /** + * Describes a shade 'capabilities' entry in the database; adds properties indicating its supported functionality. + * + * @author Andrew Fiddian-Green - Initial Contribution + */ + public static class Capabilities extends Base { + private boolean supportsPrimary; + private boolean supportsSecondary; + private boolean supportsTiltOnClosed; + private boolean supportsTiltAnywhere; + private boolean primaryStateInverted; + private boolean tilt180Degrees; + + public Capabilities() { + } + + protected Capabilities(int capabilities) { + intValue = capabilities; + } + + protected Capabilities text(String text) { + this.text = text; + return this; + } + + protected Capabilities primary() { + supportsPrimary = true; + return this; + } + + protected Capabilities tiltOnClosed() { + supportsTiltOnClosed = true; + return this; + } + + protected Capabilities secondary() { + supportsSecondary = true; + return this; + } + + protected Capabilities tiltAnywhere() { + supportsTiltAnywhere = true; + return this; + } + + protected Capabilities primaryStateInverted() { + primaryStateInverted = true; + return this; + } + + protected Capabilities tilt180() { + tilt180Degrees = true; + return this; + } + + /** + * Check if the Capabilities class instance supports a primary shade. + * + * @return true if it supports a primary shade. + */ + public boolean supportsPrimary() { + return supportsPrimary; + } + + /** + * Check if the Capabilities class instance supports a vane/tilt function (by means of a second motor). + * + * @return true if it supports a vane/tilt function (by means of a second motor). + */ + public boolean supportsTiltAnywhere() { + return supportsTiltAnywhere; + } + + /** + * Check if the Capabilities class instance supports a secondary shade. + * + * @return true if it supports a secondary shade. + */ + public boolean supportsSecondary() { + return supportsSecondary; + } + + /** + * Check if the Capabilities class instance supports a secondary shade. + * + * @return true if the primary shade is inverted. + */ + public boolean isPrimaryStateInverted() { + return primaryStateInverted; + } + + /** + * Check if the Capabilities class instance supports 'tilt when closed'. + * + * Note: Simple bottom up or vertical shades that do not have independent vane controls, can be tilted in a + * simple way, only when they are fully closed, by moving the shade motor a bit further. + * + * @return true if the primary shade is inverted. + */ + public boolean supportsTiltOnClosed() { + return supportsTiltOnClosed && !supportsTiltAnywhere; + } + + /** + * Check if the Capabilities class instance supports 180 degrees tilt. + * + * @return true if the primary shade supports 180 degrees. + */ + public boolean supportsTilt180() { + return tilt180Degrees; + } + } + + /** + * Determines if a given shade 'type' is in the database. + * + * @param type the shade 'type' parameter. + * @return true if the shade 'type' is known. + */ + public boolean isTypeInDatabase(int type) { + return TYPE_DATABASE.containsKey(type); + } + + /** + * Determines if a given 'capabilities' value is in the database. + * + * @param capabilities the shade 'capabilities' parameter + * @return true if the 'capabilities' value is known + */ + public boolean isCapabilitiesInDatabase(int capabilities) { + return CAPABILITIES_DATABASE.containsKey(capabilities); + } + + /** + * Return a Type class instance that corresponds to the given 'type' parameter. + * + * @param type the shade 'type' parameter. + * @return corresponding instance of Type class. + */ + public Type getType(int type) { + return TYPE_DATABASE.getOrDefault(type, new Type()); + } + + /** + * Return a Capabilities class instance that corresponds to the given 'capabilities' parameter. + * + * @param capabilities the shade 'capabilities' parameter. + * @return corresponding instance of Capabilities class. + */ + public Capabilities getCapabilities(int capabilities) { + return CAPABILITIES_DATABASE.getOrDefault(capabilities, new Capabilities()); + } + + private static final String REQUEST_DEVELOPERS_TO_UPDATE = " => Please request developers to update the database!"; + + /** + * Log a message indicating that 'type' is not in database. + * + * @param type + */ + public void logTypeNotInDatabase(int type) { + logger.warn("The shade 'type:{}' is not in the database!{}", type, REQUEST_DEVELOPERS_TO_UPDATE); + } + + /** + * Log a message indicating that 'capabilities' is not in database. + * + * @param capabilities + */ + public void logCapabilitiesNotInDatabase(int type, int capabilities) { + logger.warn("The 'capabilities:{}' for shade 'type:{}' are not in the database!{}", capabilities, type, + REQUEST_DEVELOPERS_TO_UPDATE); + } + + /** + * Log a message indicating the type's capabilities and the passed capabilities are not equal. + * + * @param type + * @param capabilities + */ + public void logCapabilitiesMismatch(int type, int capabilities) { + logger.warn("The 'capabilities:{}' reported by shade 'type:{}' don't match the database!{}", capabilities, type, + REQUEST_DEVELOPERS_TO_UPDATE); + } + + /** + * Log a message indicating that a shade's secondary/vanes support, as observed via its actual JSON payload, does + * not match the expected value as declared in its 'type' and 'capabilities'. + * + * @param propertyKey + * @param type + * @param capabilities + * @param propertyValue + */ + public void logPropertyMismatch(String propertyKey, int type, int capabilities, boolean propertyValue) { + logger.warn( + "The '{}:{}' property actually reported by shade 'type:{}' is different " + + "than expected from its 'capabilities:{}' in the database!{}", + propertyKey, propertyValue, type, capabilities, REQUEST_DEVELOPERS_TO_UPDATE); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java index ad4ad4e390c..5b811028b81 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewShadeDiscoveryService.java @@ -26,9 +26,10 @@ import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.api.responses.Shades; import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration; +import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; +import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler; import org.openhab.core.config.discovery.AbstractDiscoveryService; -import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.thing.ThingUID; import org.slf4j.Logger; @@ -48,6 +49,7 @@ public class HDPowerViewShadeDiscoveryService extends AbstractDiscoveryService { private final HDPowerViewHubHandler hub; private final Runnable scanner; private @Nullable ScheduledFuture backgroundFuture; + private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase(); public HDPowerViewShadeDiscoveryService(HDPowerViewHubHandler hub) { super(Collections.singleton(HDPowerViewBindingConstants.THING_TYPE_SHADE), 600, true); @@ -96,12 +98,20 @@ public class HDPowerViewShadeDiscoveryService extends AbstractDiscoveryService { String id = Integer.toString(shadeData.id); ThingUID thingUID = new ThingUID(HDPowerViewBindingConstants.THING_TYPE_SHADE, bridgeUID, id); - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID) + Integer caps = shadeData.capabilities; + Capabilities capabilities = db.getCapabilities((caps != null) ? caps.intValue() : -1); + + DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID) + .withLabel(shadeData.getName()).withBridge(bridgeUID) .withProperty(HDPowerViewShadeConfiguration.ID, id) - .withRepresentationProperty(HDPowerViewShadeConfiguration.ID) - .withLabel(shadeData.getName()).withBridge(bridgeUID).build(); + .withProperty(HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE, + db.getType(shadeData.type).toString()) + .withProperty(HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES, + capabilities.toString()) + .withRepresentationProperty(HDPowerViewShadeConfiguration.ID); + logger.debug("Hub discovered shade '{}'", id); - thingDiscovered(result); + thingDiscovered(builder.build()); } } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java index 502e2c6ff64..a617fe1711b 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java @@ -13,9 +13,9 @@ package org.openhab.binding.hdpowerview.internal.handler; import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*; -import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*; import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; +import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -23,16 +23,18 @@ import javax.ws.rs.NotSupportedException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.HubProcessingException; -import org.openhab.binding.hdpowerview.internal.api.ActuatorClass; import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem; import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.api.responses.Shade; import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.api.responses.Survey; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration; +import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; +import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -47,7 +49,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; -import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,6 +75,9 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { private @Nullable ScheduledFuture refreshSignalFuture = null; private @Nullable ScheduledFuture refreshBatteryLevelFuture = null; + private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase(); + private int shadeCapabilities = -1; + public HDPowerViewShadeHandler(Thing thing) { super(thing); } @@ -131,9 +135,9 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { switch (channelId) { case CHANNEL_SHADE_POSITION: if (command instanceof PercentType) { - moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, ((PercentType) command).intValue()); + moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue()); } else if (command instanceof UpDownType) { - moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100); + moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100); } else if (command instanceof StopMoveType) { if (StopMoveType.STOP.equals(command)) { stopShade(); @@ -145,17 +149,17 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { case CHANNEL_SHADE_VANE: if (command instanceof PercentType) { - moveShade(PRIMARY_ACTUATOR, VANE_COORDS, ((PercentType) command).intValue()); + moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue()); } else if (command instanceof OnOffType) { - moveShade(PRIMARY_ACTUATOR, VANE_COORDS, OnOffType.ON.equals(command) ? 100 : 0); + moveShade(VANE_TILT_COORDS, OnOffType.ON.equals(command) ? 100 : 0); } break; case CHANNEL_SHADE_SECONDARY_POSITION: if (command instanceof PercentType) { - moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, ((PercentType) command).intValue()); + moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue()); } else if (command instanceof UpDownType) { - moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100); + moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100); } else if (command instanceof StopMoveType) { if (StopMoveType.STOP.equals(command)) { stopShade(); @@ -168,13 +172,14 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } /** - * Update the state of the channels based on the ShadeData provided + * Update the state of the channels based on the ShadeData provided. * - * @param shadeData the ShadeData to be used; may be null + * @param shadeData the ShadeData to be used; may be null. */ protected void onReceiveUpdate(@Nullable ShadeData shadeData) { if (shadeData != null) { updateStatus(ThingStatus.ONLINE); + updateSoftProperties(shadeData); updateBindingStates(shadeData.positions); updateBatteryLevel(shadeData.batteryStatus); updateState(CHANNEL_SHADE_BATTERY_VOLTAGE, @@ -186,16 +191,116 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } } - private void updateBindingStates(@Nullable ShadePosition shadePos) { - if (shadePos != null) { - updateState(CHANNEL_SHADE_POSITION, shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED)); - updateState(CHANNEL_SHADE_VANE, shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS)); - updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN)); - } else { - updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF); - updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF); - updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF); + /** + * Update the Thing's properties based on the contents of the provided ShadeData. + * + * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so + * that developers can be kept updated about the potential need to add support for that type resp. capabilities. + * + * @param shadeData + */ + private void updateSoftProperties(ShadeData shadeData) { + final Map properties = getThing().getProperties(); + boolean propChanged = false; + + // update 'type' property + final int type = shadeData.type; + String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE; + String propOldVal = properties.getOrDefault(propKey, ""); + String propNewVal = db.getType(type).toString(); + if (!propNewVal.equals(propOldVal)) { + propChanged = true; + getThing().setProperty(propKey, propNewVal); + if ((type > 0) && !db.isTypeInDatabase(type)) { + db.logTypeNotInDatabase(type); + } } + + // update 'capabilities' property + final Integer temp = shadeData.capabilities; + final int capabilitiesVal = temp != null ? temp.intValue() : -1; + Capabilities capabilities = db.getCapabilities(capabilitiesVal); + propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES; + propOldVal = properties.getOrDefault(propKey, ""); + propNewVal = capabilities.toString(); + if (!propNewVal.equals(propOldVal)) { + propChanged = true; + getThing().setProperty(propKey, propNewVal); + if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) { + db.logCapabilitiesNotInDatabase(type, capabilitiesVal); + } + } + + // update shadeCapabilities field + if (capabilitiesVal >= 0) { + shadeCapabilities = capabilitiesVal; + } + + if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type) + && (capabilitiesVal != db.getType(type).getCapabilities())) { + db.logCapabilitiesMismatch(type, capabilitiesVal); + } + } + + /** + * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData. + * + * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches + * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the + * potential need to add support for that type resp. capabilities. + * + * @param shadeData + */ + private void updateHardProperties(ShadeData shadeData) { + final ShadePosition positions = shadeData.positions; + if (positions != null) { + final Map properties = getThing().getProperties(); + + // update 'jsonHasSecondary' property + String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED; + String propOldVal = properties.getOrDefault(propKey, ""); + boolean propNewBool = positions.secondaryRailDetected(); + String propNewVal = String.valueOf(propNewBool); + if (!propNewVal.equals(propOldVal)) { + getThing().setProperty(propKey, propNewVal); + final Integer temp = shadeData.capabilities; + final int capabilities = temp != null ? temp.intValue() : -1; + if (propNewBool != db.getCapabilities(capabilities).supportsSecondary()) { + db.logPropertyMismatch(propKey, shadeData.type, capabilities, propNewBool); + } + } + + // update 'jsonTiltAnywhere' property + propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED; + propOldVal = properties.getOrDefault(propKey, ""); + propNewBool = positions.tiltAnywhereDetected(); + propNewVal = String.valueOf(propNewBool); + if (!propNewVal.equals(propOldVal)) { + getThing().setProperty(propKey, propNewVal); + final Integer temp = shadeData.capabilities; + final int capabilities = temp != null ? temp.intValue() : -1; + if (propNewBool != db.getCapabilities(capabilities).supportsTiltAnywhere()) { + db.logPropertyMismatch(propKey, shadeData.type, capabilities, propNewBool); + } + } + } + } + + private void updateBindingStates(@Nullable ShadePosition shadePos) { + if (shadePos == null) { + logger.debug("The value of 'shadePosition' argument was null!"); + } else if (shadeCapabilities < 0) { + logger.debug("The 'shadeCapabilities' field has not been initialized!"); + } else { + Capabilities caps = db.getCapabilities(shadeCapabilities); + updateState(CHANNEL_SHADE_POSITION, shadePos.getState(caps, PRIMARY_ZERO_IS_CLOSED)); + updateState(CHANNEL_SHADE_VANE, shadePos.getState(caps, VANE_TILT_COORDS)); + updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(caps, SECONDARY_ZERO_IS_OPEN)); + return; + } + updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF); + updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF); + updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF); } private void updateBatteryLevel(int batteryStatus) { @@ -220,7 +325,7 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue)); } - private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) { + private void moveShade(CoordinateSystem coordSys, int newPercent) { try { HDPowerViewHubHandler bridge; if ((bridge = getBridgeHandler()) == null) { @@ -230,33 +335,28 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { if (webTargets == null) { throw new HubProcessingException("Web targets not initialized"); } + ShadePosition newPosition = null; + // (try to) read the positions from the hub int shadeId = getShadeId(); - - switch (actuatorClass) { - case PRIMARY_ACTUATOR: - // write the new primary position - webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent)); - break; - case SECONDARY_ACTUATOR: - // read the current primary position; default value 100% - int primaryPercent = 100; - Shade shade = webTargets.getShade(shadeId); - if (shade != null) { - ShadeData shadeData = shade.shade; - if (shadeData != null) { - ShadePosition shadePos = shadeData.positions; - if (shadePos != null) { - State primaryState = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - if (primaryState instanceof PercentType) { - primaryPercent = ((PercentType) primaryState).intValue(); - } - } - } - } - // write the current primary position, plus the new secondary position - webTargets.moveShade(shadeId, - ShadePosition.create(ZERO_IS_CLOSED, primaryPercent, ZERO_IS_OPEN, newPercent)); + Shade shade = webTargets.getShade(shadeId); + if (shade != null) { + ShadeData shadeData = shade.shade; + if (shadeData != null) { + newPosition = shadeData.positions; + } } + // if no positions returned, then create a new position + if (newPosition == null) { + newPosition = new ShadePosition(); + } + // set the new position value, and write the positions to the hub + webTargets.moveShade(shadeId, + newPosition.setPosition(db.getCapabilities(shadeCapabilities), coordSys, newPercent)); + // update the Channels to match the new position + final ShadePosition finalPosition = newPosition; + scheduler.submit(() -> { + updateBindingStates(finalPosition); + }); } catch (HubProcessingException | NumberFormatException e) { logger.warn("Unexpected error: {}", e.getMessage()); return; @@ -375,6 +475,8 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { if (shadeData != null) { if (Boolean.TRUE.equals(shadeData.timedOut)) { logger.warn("Shade {} wireless refresh time out", shadeId); + } else if (kind == RefreshKind.POSITION) { + updateHardProperties(shadeData); } } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml index 6769c07c9a0..8e4cc60693d 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml @@ -69,6 +69,10 @@ Hunter Douglas (Luxaflex) PowerView Motorized Shade + + + + id diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java index 8bd12d92b3a..516b850e4b9 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java @@ -13,7 +13,6 @@ package org.openhab.binding.hdpowerview; import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*; import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; import java.io.IOException; @@ -24,13 +23,11 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.junit.jupiter.api.Test; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.HubProcessingException; -import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem; import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections; import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; @@ -39,6 +36,8 @@ import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; import org.openhab.binding.hdpowerview.internal.api.responses.Shade; import org.openhab.binding.hdpowerview.internal.api.responses.Shades; import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; +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; @@ -47,7 +46,7 @@ import com.google.gson.Gson; import com.google.gson.JsonParseException; /** - * Unit tests for HD PowerView binding + * Unit tests for HD PowerView binding. * * @author Andrew Fiddian-Green - Initial contribution * @author Jacob Laursen - Add support for scene groups @@ -58,8 +57,10 @@ 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 + * load a test JSON string from a file. */ private String loadJson(String fileName) { try { @@ -72,7 +73,7 @@ public class HDPowerViewJUnitTests { } /** - * Run a series of ONLINE tests on the communication with a hub + * Run a series of ONLINE tests on the communication with a hub. * * @param hubIPAddress must be a valid hub IP address to run the * tests on; or an INVALID IP address to @@ -111,79 +112,36 @@ public class HDPowerViewJUnitTests { HDPowerViewWebTargets webTargets = new HDPowerViewWebTargets(client, hubIPAddress); assertNotNull(webTargets); - // ==== exercise some code ==== - ShadePosition test; - State pos; - - // shade fully up - test = ShadePosition.create(ZERO_IS_CLOSED, 0); - assertNotNull(test); - pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(0, ((PercentType) pos).intValue()); - pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS); - assertTrue(UnDefType.UNDEF.equals(pos)); - - // shade fully down (method 1) - test = ShadePosition.create(ZERO_IS_CLOSED, 100); - assertNotNull(test); - pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(100, ((PercentType) pos).intValue()); - pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(0, ((PercentType) pos).intValue()); - - // shade fully down (method 2) - test = ShadePosition.create(VANE_COORDS, 0); - assertNotNull(test); - pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(100, ((PercentType) pos).intValue()); - pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(0, ((PercentType) pos).intValue()); - - // shade fully down (method 2) and vane fully open - test = ShadePosition.create(VANE_COORDS, 100); - assertNotNull(test); - pos = test.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(100, ((PercentType) pos).intValue()); - pos = test.getState(PRIMARY_ACTUATOR, VANE_COORDS); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(100, ((PercentType) pos).intValue()); - int shadeId = 0; - @Nullable ShadePosition shadePos = null; - @Nullable Shades shadesX = null; // ==== get all shades ==== try { shadesX = webTargets.getShades(); assertNotNull(shadesX); - @Nullable - List shadesData = shadesX.shadeData; - assertNotNull(shadesData); - assertTrue(!shadesData.isEmpty()); - @Nullable - ShadeData shadeData; - shadeData = shadesData.get(0); - assertNotNull(shadeData); - assertTrue(shadeData.getName().length() > 0); - shadePos = shadeData.positions; - assertNotNull(shadePos); - @Nullable - ShadeData shadeZero = shadesData.get(0); - assertNotNull(shadeZero); - shadeId = shadeZero.id; - assertNotEquals(0, shadeId); + if (shadesX != null) { + List shadesData = shadesX.shadeData; + assertNotNull(shadesData); - for (ShadeData shadexData : shadesData) { - String shadeName = shadexData.getName(); - assertNotNull(shadeName); + if (shadesData != null) { + assertTrue(!shadesData.isEmpty()); + ShadeData shadeData; + shadeData = shadesData.get(0); + assertNotNull(shadeData); + assertTrue(shadeData.getName().length() > 0); + shadePos = shadeData.positions; + assertNotNull(shadePos); + ShadeData shadeZero = shadesData.get(0); + assertNotNull(shadeZero); + shadeId = shadeZero.id; + assertNotEquals(0, shadeId); + + for (ShadeData shadexData : shadesData) { + String shadeName = shadexData.getName(); + assertNotNull(shadeName); + } + } } } catch (JsonParseException | HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); @@ -194,26 +152,29 @@ public class HDPowerViewJUnitTests { try { Scenes scenes = webTargets.getScenes(); assertNotNull(scenes); - @Nullable - List scenesData = scenes.sceneData; - assertNotNull(scenesData); - assertTrue(!scenesData.isEmpty()); - @Nullable - Scene sceneZero = scenesData.get(0); - assertNotNull(sceneZero); - sceneId = sceneZero.id; - assertTrue(sceneId > 0); - for (Scene scene : scenesData) { - String sceneName = scene.getName(); - assertNotNull(sceneName); + if (scenes != null) { + List scenesData = scenes.sceneData; + assertNotNull(scenesData); + + if (scenesData != null) { + assertTrue(!scenesData.isEmpty()); + Scene sceneZero = scenesData.get(0); + assertNotNull(sceneZero); + sceneId = sceneZero.id; + assertTrue(sceneId > 0); + + for (Scene scene : scenesData) { + String sceneName = scene.getName(); + assertNotNull(sceneName); + } + } } } catch (JsonParseException | HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); } // ==== refresh a specific shade ==== - @Nullable Shade shade = null; try { assertNotEquals(0, shadeId); @@ -227,28 +188,53 @@ public class HDPowerViewJUnitTests { try { assertNotEquals(0, shadeId); assertNotNull(shade); - @Nullable - ShadeData shadeData = shade.shade; - assertNotNull(shadeData); - ShadePosition positions = shadeData.positions; - assertNotNull(positions); - CoordinateSystem coordSys = positions.getCoordinateSystem(PRIMARY_ACTUATOR); - assertNotNull(coordSys); + if (shade != null) { + ShadeData shadeData = shade.shade; + assertNotNull(shadeData); - pos = positions.getState(PRIMARY_ACTUATOR, coordSys); - assertEquals(PercentType.class, pos.getClass()); + if (shadeData != null) { + ShadePosition positions = shadeData.positions; + assertNotNull(positions); - pos = positions.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - assertEquals(PercentType.class, pos.getClass()); + if (positions != null) { + Integer capabilitiesValue = shadeData.capabilities; + assertNotNull(capabilitiesValue); - int position = ((PercentType) pos).intValue(); - position = position + ((position <= 10) ? 5 : -5); + if (capabilitiesValue != null) { + Capabilities capabilities = db.getCapabilities(capabilitiesValue.intValue()); - ShadePosition newPos = ShadePosition.create(ZERO_IS_CLOSED, position); - assertNotNull(newPos); + State pos = positions.getState(capabilities, PRIMARY_ZERO_IS_CLOSED); + assertEquals(PercentType.class, pos.getClass()); - if (allowShadeMovementCommands) { - webTargets.moveShade(shadeId, newPos); + int position = ((PercentType) pos).intValue(); + position = position + ((position <= 10) ? 5 : -5); + + ShadePosition targetPosition = new ShadePosition().setPosition(capabilities, + PRIMARY_ZERO_IS_CLOSED, position); + assertNotNull(targetPosition); + + if (allowShadeMovementCommands) { + webTargets.moveShade(shadeId, targetPosition); + + Shade newShade = webTargets.getShade(shadeId); + assertNotNull(newShade); + if (newShade != null) { + ShadeData newData = newShade.shade; + assertNotNull(newData); + if (newData != null) { + ShadePosition actualPosition = newData.positions; + assertNotNull(actualPosition); + if (actualPosition != null) { + assertEquals( + targetPosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED), + actualPosition.getState(capabilities, PRIMARY_ZERO_IS_CLOSED)); + } + } + } + } + } + } + } } } catch (HubProcessingException | HubMaintenanceException e) { fail(e.getMessage()); @@ -286,12 +272,78 @@ public class HDPowerViewJUnitTests { } /** - * Test generic JSON shades response + * 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. */ @Test public void shadeResponseIsParsedCorrectly() throws JsonParseException { final Gson gson = new Gson(); - @Nullable Shades shades; String json = loadJson("shades"); assertNotEquals("", json); @@ -300,7 +352,7 @@ public class HDPowerViewJUnitTests { } /** - * Test generic JSON scene response + * Test generic JSON scene response. */ @Test public void sceneResponseIsParsedCorrectly() throws JsonParseException { @@ -308,23 +360,22 @@ public class HDPowerViewJUnitTests { String json = loadJson("scenes"); assertNotEquals("", json); - @Nullable Scenes scenes = gson.fromJson(json, Scenes.class); assertNotNull(scenes); - - @Nullable - List sceneData = scenes.sceneData; - assertNotNull(sceneData); - - assertEquals(4, sceneData.size()); - @Nullable - Scene scene = sceneData.get(0); - assertEquals("Door Open", scene.getName()); - assertEquals(18097, scene.id); + if (scenes != null) { + List sceneData = scenes.sceneData; + assertNotNull(sceneData); + if (sceneData != null) { + assertEquals(4, sceneData.size()); + Scene scene = sceneData.get(0); + assertEquals("Door Open", scene.getName()); + assertEquals(18097, scene.id); + } + } } /** - * Test generic JSON scene collection response + * Test generic JSON scene collection response. */ @Test public void sceneCollectionResponseIsParsedCorrectly() throws JsonParseException { @@ -332,22 +383,24 @@ public class HDPowerViewJUnitTests { String json = loadJson("sceneCollections"); assertNotEquals("", json); - @Nullable SceneCollections sceneCollections = gson.fromJson(json, SceneCollections.class); assertNotNull(sceneCollections); - @Nullable - List sceneCollectionData = sceneCollections.sceneCollectionData; - assertNotNull(sceneCollectionData); - assertEquals(1, sceneCollectionData.size()); - @Nullable - SceneCollection sceneCollection = sceneCollectionData.get(0); - assertEquals("Børn op", sceneCollection.getName()); - assertEquals(27119, sceneCollection.id); + if (sceneCollections != null) { + List sceneCollectionData = sceneCollections.sceneCollectionData; + assertNotNull(sceneCollectionData); + if (sceneCollectionData != null) { + assertEquals(1, sceneCollectionData.size()); + + SceneCollection sceneCollection = sceneCollectionData.get(0); + assertEquals("Børn op", sceneCollection.getName()); + assertEquals(27119, sceneCollection.id); + } + } } /** - * Test the JSON parsing for a duette top down bottom up shade + * Test the JSON parsing for a duette top down bottom up shade. */ @Test public void duetteTopDownBottomUpShadeIsParsedCorrectly() throws JsonParseException { @@ -355,38 +408,157 @@ public class HDPowerViewJUnitTests { String json = loadJson("duette"); assertNotEquals("", json); - @Nullable Shades shades = gson.fromJson(json, Shades.class); assertNotNull(shades); - @Nullable - List shadesData = shades.shadeData; - assertNotNull(shadesData); + if (shades != null) { + List shadesData = shades.shadeData; + assertNotNull(shadesData); - assertEquals(1, shadesData.size()); - @Nullable - ShadeData shadeData = shadesData.get(0); - assertNotNull(shadeData); + if (shadesData != null) { + assertEquals(1, shadesData.size()); + ShadeData shadeData = shadesData.get(0); + assertNotNull(shadeData); - assertEquals("Gardin 1", shadeData.getName()); - assertEquals(63778, shadeData.id); + assertEquals("Gardin 1", shadeData.getName()); + assertEquals(63778, shadeData.id); - ShadePosition shadePos = shadeData.positions; - assertNotNull(shadePos); - assertEquals(ZERO_IS_CLOSED, shadePos.getCoordinateSystem(PRIMARY_ACTUATOR)); + ShadePosition shadePos = shadeData.positions; + assertNotNull(shadePos); - State pos = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(59, ((PercentType) pos).intValue()); + if (shadePos != null) { + Integer capabilitiesValue = shadeData.capabilities; + assertNotNull(capabilitiesValue); + if (capabilitiesValue != null) { + assertEquals(7, capabilitiesValue.intValue()); - pos = shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN); - assertEquals(PercentType.class, pos.getClass()); - assertEquals(35, ((PercentType) pos).intValue()); + Capabilities capabilities = db.getCapabilities(capabilitiesValue); - pos = shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS); - assertEquals(UnDefType.class, pos.getClass()); + State pos = shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED); + assertEquals(PercentType.class, pos.getClass()); + assertEquals(59, ((PercentType) pos).intValue()); - assertEquals(3, shadeData.batteryStatus); + pos = shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN); + assertEquals(PercentType.class, pos.getClass()); + assertEquals(35, ((PercentType) pos).intValue()); - assertEquals(4, shadeData.signalStrength); + pos = shadePos.getState(capabilities, VANE_TILT_COORDS); + assertEquals(UnDefType.class, pos.getClass()); + + assertEquals(3, shadeData.batteryStatus); + + assertEquals(4, shadeData.signalStrength); + + assertEquals(8, shadeData.type); + + assertTrue(db.isTypeInDatabase(shadeData.type)); + assertTrue(db.isCapabilitiesInDatabase(capabilitiesValue.intValue())); + + assertEquals(db.getType(shadeData.type).getCapabilities(), capabilitiesValue.intValue()); + + assertTrue(db.getCapabilities(capabilitiesValue.intValue()).supportsSecondary()); + assertNotEquals(db.getType(shadeData.type).getCapabilities(), capabilitiesValue.intValue() + 1); + + // ==== when changing position1, position2 value is not changed (vice-versa) ==== + ShadePosition shadePosition = shadeData.positions; + 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); + 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); + assertEquals(PercentType.class, position1Old.getClass()); + assertEquals(PercentType.class, position1New.getClass()); + assertEquals(((PercentType) position1Old).intValue(), + ((PercentType) position1New).intValue()); + } + } + } + } + } + } + + /** + * 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)); } }