]> git.basschouten.com Git - openhab-addons.git/commitdiff
[hdpowerview] Add support for calibrating a shade (#12002)
authorJacob Laursen <jacob-github@vindvejr.dk>
Fri, 14 Jan 2022 21:59:01 +0000 (22:59 +0100)
committerGitHub <noreply@github.com>
Fri, 14 Jan 2022 21:59:01 +0000 (22:59 +0100)
* Add support for calibrating a shade.

Fixes #11767

* Fix startup problems by decoupling capabilities cache from updateSoftProperties.
* Minor refactoring of capabilities and shade id handling.
* Dispose faster/safer by killing any remaining tasks.
* Set shade thing status to UNKNOWN until we receive any data for shade.
* Fix position update glitch after setting position.
* Remove unneeded catch after shade id refactoring.
* Document return values in Javadoc.
* Avoid logging InterruptedException during dispose.
* Add calibration example item.
* Reduce nesting.
* Add myself as reviewer for binding.
* Add Andrew Fiddian-Green as reviewer for binding.
* Handle JsonParseException.
* Fix alphabetic order.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
15 files changed:
CODEOWNERS
bundles/org.openhab.binding.hdpowerview/README.md
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdPosition.java [deleted file]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdStop.java [deleted file]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.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/main/resources/OH-INF/i18n/hdpowerview.properties
bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml

index 0f1b2fe4d25185d3e0f69032b2ca44de5890aff0..ae972027ead62da8205c996a06c798789f0fa0ce 100644 (file)
 /bundles/org.openhab.binding.haywardomnilogic/ @matchews
 /bundles/org.openhab.binding.hccrubbishcollection/ @cossey
 /bundles/org.openhab.binding.hdanywhere/ @kgoderis
-/bundles/org.openhab.binding.hdpowerview/ @beowulfe
+/bundles/org.openhab.binding.hdpowerview/ @beowulfe @jlaur @andrewfg
 /bundles/org.openhab.binding.helios/ @kgoderis
 /bundles/org.openhab.binding.heliosventilation/ @ramack
 /bundles/org.openhab.binding.heos/ @Wire82
index 1c70a3a68f684a0c6587fce67462b0de63a19221..46f0e27e8926c36baeed8bb045f71ec68f612090 100644 (file)
@@ -82,6 +82,7 @@ All of these channels appear in the binding, but only those which have a physica
 | 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 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). |
+| calibrate      | Switch                   | Setting this to ON will calibrate the shade. Note: include `{autoupdate="false"}` in the item configuration to avoid having to reset it to off after use. |
 | 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)
 | batteryVoltage | Number:ElectricPotential | Battery voltage reported by the shade. |
@@ -205,6 +206,8 @@ Rollershutter Living_Room_Shade_Secondary "Living Room Shade Secondary Position
 Dimmer Living_Room_Shade_Vane "Living Room Shade Vane [%.0f %%]" {channel="hdpowerview:shade:g24:s50150:vane"}
 
 Switch Living_Room_Shade_Battery_Low_Alarm "Living Room Shade Battery Low Alarm [%s]" {channel="hdpowerview:shade:g24:s50150:lowBattery"}
+
+Switch Living_Room_Shade_Calibrate "Living Room Shade Calibrate" {channel="hdpowerview:shade:g24:s50150:calibrate", autoupdate="false"}
 ```
 
 Scene items:
index 2cc89626191dfdf89ab5416689efe0b4cde74813..371f63d7176359abeeebb7491001b0626c320587 100644 (file)
@@ -41,6 +41,7 @@ public class HDPowerViewBindingConstants {
     public static final String CHANNEL_SHADE_POSITION = "position";
     public static final String CHANNEL_SHADE_SECONDARY_POSITION = "secondary";
     public static final String CHANNEL_SHADE_VANE = "vane";
+    public static final String CHANNEL_SHADE_CALIBRATE = "calibrate";
     public static final String CHANNEL_SHADE_LOW_BATTERY = "lowBattery";
     public static final String CHANNEL_SHADE_BATTERY_LEVEL = "batteryLevel";
     public static final String CHANNEL_SHADE_BATTERY_VOLTAGE = "batteryVoltage";
index d7b3c1eaa81ca301283be003a647e62686677299..deabb7fb7778abcfe67c0739fb235c37de59fe24 100644 (file)
@@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
+import org.openhab.binding.hdpowerview.internal.api.requests.ShadeCalibrate;
 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove;
 import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
 import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersion;
@@ -169,12 +170,45 @@ public class HDPowerViewWebTargets {
      *
      * @param shadeId id of the shade to be moved
      * @param position instance of ShadePosition containing the new position
+     * @return Shade class instance (with new position)
      * @throws HubProcessingException if there is any processing error
      * @throws HubMaintenanceException if the hub is down for maintenance
      */
-    public void moveShade(int shadeId, ShadePosition position) throws HubProcessingException, HubMaintenanceException {
-        String json = gson.toJson(new ShadeMove(shadeId, position));
-        invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
+    public @Nullable Shade moveShade(int shadeId, ShadePosition position)
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        String jsonRequest = gson.toJson(new ShadeMove(position));
+        String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
+        return gson.fromJson(jsonResponse, Shade.class);
+    }
+
+    /**
+     * Instructs the hub to stop movement of a specific shade
+     *
+     * @param shadeId id of the shade to be stopped
+     * @return Shade class instance (new position cannot be relied upon)
+     * @throws HubProcessingException if there is any processing error
+     * @throws HubMaintenanceException if the hub is down for maintenance
+     */
+    public @Nullable Shade stopShade(int shadeId)
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        String jsonRequest = gson.toJson(new ShadeStop());
+        String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
+        return gson.fromJson(jsonResponse, Shade.class);
+    }
+
+    /**
+     * Instructs the hub to calibrate a specific shade
+     *
+     * @param shadeId id of the shade to be calibrated
+     * @return Shade class instance
+     * @throws HubProcessingException if there is any processing error
+     * @throws HubMaintenanceException if the hub is down for maintenance
+     */
+    public @Nullable Shade calibrateShade(int shadeId)
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        String jsonRequest = gson.toJson(new ShadeCalibrate());
+        String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest);
+        return gson.fromJson(jsonResponse, Shade.class);
     }
 
     /**
@@ -397,16 +431,4 @@ public class HDPowerViewWebTargets {
                 Query.of("updateBatteryLevel", Boolean.toString(true)), null);
         return gson.fromJson(json, Shade.class);
     }
-
-    /**
-     * Tells the hub to stop movement of a specific shade
-     *
-     * @param shadeId id of the shade to be stopped
-     * @throws HubProcessingException if there is any processing error
-     * @throws HubMaintenanceException if the hub is down for maintenance
-     */
-    public void stopShade(int shadeId) throws HubProcessingException, HubMaintenanceException {
-        String json = gson.toJson(new ShadeStop(shadeId));
-        invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, json);
-    }
 }
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java
new file mode 100644 (file)
index 0000000..e1ec80e
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * 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.internal.api.requests;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A request to calibrate a shade
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class ShadeCalibrate {
+
+    public ShadeMotion shade;
+
+    public ShadeCalibrate() {
+        this.shade = new ShadeMotion(ShadeMotion.Type.CALIBRATE);
+    }
+}
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdPosition.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdPosition.java
deleted file mode 100644 (file)
index 644d45f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * 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.internal.api.requests;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
-
-/**
- * The position of a shade to set
- *
- * @author Andy Lintner - Initial contribution
- */
-@NonNullByDefault
-class ShadeIdPosition {
-
-    int id;
-    public @Nullable ShadePosition positions;
-
-    public ShadeIdPosition(int id, ShadePosition position) {
-        this.id = id;
-        this.positions = position;
-    }
-}
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdStop.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdStop.java
deleted file mode 100644 (file)
index 6606440..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * 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.internal.api.requests;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * The motion "stop" directive for a shade
- *
- * @author Andrew Fiddian-Green - Initial contribution
- */
-@NonNullByDefault
-class ShadeIdStop {
-
-    int id;
-    public @Nullable String motion;
-
-    public ShadeIdStop(int id) {
-        this.id = id;
-        this.motion = "stop";
-    }
-}
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java
new file mode 100644 (file)
index 0000000..f4c9d78
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * 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.internal.api.requests;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A motion directive for a shade
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+class ShadeMotion {
+
+    public enum Type {
+        STOP("stop"),
+        CALIBRATE("calibrate");
+
+        private String motion;
+
+        Type(String motion) {
+            this.motion = motion;
+        }
+
+        public String getMotion() {
+            return this.motion;
+        }
+    }
+
+    public String motion;
+
+    public ShadeMotion(Type motionType) {
+        this.motion = motionType.getMotion();
+    }
+}
index c70d31cd1154a955ddd30af590ccf368f530539a..fe688f4f75b585b11f882b861ac8ae96d8121ea3 100644 (file)
@@ -13,7 +13,6 @@
 package org.openhab.binding.hdpowerview.internal.api.requests;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
 
 /**
@@ -24,9 +23,9 @@ import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
 @NonNullByDefault
 public class ShadeMove {
 
-    public @Nullable ShadeIdPosition shade;
+    public ShadePositions shade;
 
-    public ShadeMove(int id, ShadePosition position) {
-        this.shade = new ShadeIdPosition(id, position);
+    public ShadeMove(ShadePosition position) {
+        this.shade = new ShadePositions(position);
     }
 }
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java
new file mode 100644 (file)
index 0000000..1c544c0
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * 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.internal.api.requests;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
+
+/**
+ * The position of a shade to set
+ *
+ * @author Andy Lintner - Initial contribution
+ */
+@NonNullByDefault
+class ShadePositions {
+
+    public ShadePosition positions;
+
+    public ShadePositions(ShadePosition position) {
+        this.positions = position;
+    }
+}
index 98a183f70c45c449a8ae5457d9ddfc10509d6d19..ed7ab4c4b0a8a2655fc0ac3230869b0b65f44c7d 100644 (file)
@@ -13,7 +13,6 @@
 package org.openhab.binding.hdpowerview.internal.api.requests;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
 
 /**
  * A request to stop the movement of a shade
@@ -23,9 +22,9 @@ import org.eclipse.jdt.annotation.Nullable;
 @NonNullByDefault
 public class ShadeStop {
 
-    public @Nullable ShadeIdStop shade;
+    public ShadeMotion shade;
 
-    public ShadeStop(int id) {
-        this.shade = new ShadeIdStop(id);
+    public ShadeStop() {
+        this.shade = new ShadeMotion(ShadeMotion.Type.STOP);
     }
 }
index 0a1cf903abfe4ff08b8cfd9daf42b0145a70349f..7326b44710b06ae60b0d6b2e328a5f0883449765 100644 (file)
@@ -92,7 +92,7 @@ public class ShadeCapabilitiesDatabase {
         protected int intValue = -1;
         protected String text = "-- not in database --";
 
-        protected Integer getValue() {
+        public Integer getValue() {
             return intValue;
         }
 
index f3b2705c1d2519778a1d0985f2c410b22a832ca3..e5e9f7ceee9098948ec05bcddd5d033dcde18341 100644 (file)
@@ -54,6 +54,8 @@ import org.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.gson.JsonParseException;
+
 /**
  * Handles commands for an HD PowerView Shade
  *
@@ -70,14 +72,14 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
     }
 
     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
+    private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
 
-    private static final int REFRESH_DELAY_SEC = 10;
     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
     private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
-
-    private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
-    private int shadeCapabilities = -1;
+    private @Nullable Capabilities capabilities;
+    private int shadeId;
+    private boolean isDisposing;
 
     public HDPowerViewShadeHandler(Thing thing) {
         super(thing);
@@ -85,8 +87,10 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
 
     @Override
     public void initialize() {
+        logger.debug("Initializing shade handler");
+        isDisposing = false;
         try {
-            getShadeId();
+            shadeId = getShadeId();
         } catch (NumberFormatException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                     "@text/offline.conf-error.invalid-id");
@@ -104,12 +108,34 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
         }
         ThingStatus bridgeStatus = bridge.getStatus();
         if (bridgeStatus == ThingStatus.ONLINE) {
-            updateStatus(ThingStatus.ONLINE);
+            updateStatus(ThingStatus.UNKNOWN);
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
         }
     }
 
+    @Override
+    public void dispose() {
+        logger.debug("Disposing shade handler for shade {}", shadeId);
+        isDisposing = true;
+        ScheduledFuture<?> future = refreshPositionFuture;
+        if (future != null) {
+            future.cancel(true);
+        }
+        refreshPositionFuture = null;
+        future = refreshSignalFuture;
+        if (future != null) {
+            future.cancel(true);
+        }
+        refreshSignalFuture = null;
+        future = refreshBatteryLevelFuture;
+        if (future != null) {
+            future.cancel(true);
+        }
+        refreshBatteryLevelFuture = null;
+        capabilities = null;
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         String channelId = channelUID.getId();
@@ -133,15 +159,42 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
             return;
         }
 
+        HDPowerViewHubHandler bridge = getBridgeHandler();
+        if (bridge == null) {
+            logger.warn("Missing bridge handler");
+            return;
+        }
+        HDPowerViewWebTargets webTargets = bridge.getWebTargets();
+        if (webTargets == null) {
+            logger.warn("Web targets not initialized");
+            return;
+        }
+        try {
+            handleShadeCommand(channelId, command, webTargets, shadeId);
+        } catch (JsonParseException e) {
+            logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
+        } catch (HubProcessingException e) {
+            // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
+            // for any ongoing requests. Logging this would only cause confusion.
+            if (!isDisposing) {
+                logger.warn("Unexpected error: {}", e.getMessage());
+            }
+        } catch (HubMaintenanceException e) {
+            // exceptions are logged in HDPowerViewWebTargets
+        }
+    }
+
+    private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
+            throws HubProcessingException, HubMaintenanceException {
         switch (channelId) {
             case CHANNEL_SHADE_POSITION:
                 if (command instanceof PercentType) {
-                    moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue());
+                    moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue(), webTargets, shadeId);
                 } else if (command instanceof UpDownType) {
-                    moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
+                    moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
                 } else if (command instanceof StopMoveType) {
-                    if (StopMoveType.STOP.equals(command)) {
-                        stopShade();
+                    if (StopMoveType.STOP == command) {
+                        stopShade(webTargets, shadeId);
                     } else {
                         logger.warn("Unexpected StopMoveType command");
                     }
@@ -150,25 +203,31 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
 
             case CHANNEL_SHADE_VANE:
                 if (command instanceof PercentType) {
-                    moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue());
+                    moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue(), webTargets, shadeId);
                 } else if (command instanceof OnOffType) {
-                    moveShade(VANE_TILT_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
+                    moveShade(VANE_TILT_COORDS, 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());
+                    moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue(), webTargets, shadeId);
                 } else if (command instanceof UpDownType) {
-                    moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
+                    moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
                 } else if (command instanceof StopMoveType) {
-                    if (StopMoveType.STOP.equals(command)) {
-                        stopShade();
+                    if (StopMoveType.STOP == command) {
+                        stopShade(webTargets, shadeId);
                     } else {
                         logger.warn("Unexpected StopMoveType command");
                     }
                 }
                 break;
+
+            case CHANNEL_SHADE_CALIBRATE:
+                if (OnOffType.ON == command) {
+                    calibrateShade(webTargets, shadeId);
+                }
+                break;
         }
     }
 
@@ -180,10 +239,14 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
     protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
         if (shadeData != null) {
             updateStatus(ThingStatus.ONLINE);
+            updateCapabilities(shadeData);
             updateSoftProperties(shadeData);
             updateFirmwareProperties(shadeData);
-            updateBindingStates(shadeData.positions);
-            updateBatteryLevel(shadeData.batteryStatus);
+            ShadePosition shadePosition = shadeData.positions;
+            if (shadePosition != null) {
+                updatePositionStates(shadePosition);
+            }
+            updateBatteryLevelStates(shadeData.batteryStatus);
             updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
                     shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
                             : UnDefType.UNDEF);
@@ -193,6 +256,29 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
         }
     }
 
+    private void updateCapabilities(ShadeData shade) {
+        if (capabilities != null) {
+            // Already cached.
+            return;
+        }
+        Integer value = shade.capabilities;
+        if (value != null) {
+            int valueAsInt = value.intValue();
+            logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
+            capabilities = db.getCapabilities(valueAsInt);
+        } else {
+            logger.debug("Capabilities not included in shade response");
+        }
+    }
+
+    private Capabilities getCapabilitiesOrDefault() {
+        Capabilities capabilities = this.capabilities;
+        if (capabilities == null) {
+            return new Capabilities();
+        }
+        return capabilities;
+    }
+
     /**
      * Update the Thing's properties based on the contents of the provided ShadeData.
      *
@@ -233,11 +319,6 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
             }
         }
 
-        // 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);
@@ -268,57 +349,52 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
      */
     private void updateHardProperties(ShadeData shadeData) {
         final ShadePosition positions = shadeData.positions;
-        if (positions != null) {
-            final Map<String, String> 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);
-                }
+        if (positions == null) {
+            return;
+        }
+        Capabilities capabilities = getCapabilitiesOrDefault();
+        final Map<String, String> properties = getThing().getProperties();
+
+        // update 'secondary rail detected' 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);
+            if (propNewBool != capabilities.supportsSecondary()) {
+                db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), 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);
-                }
+        // update 'tilt anywhere detected' 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);
+            if (propNewBool != capabilities.supportsTiltAnywhere()) {
+                db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), 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));
+    private void updatePositionStates(ShadePosition shadePos) {
+        Capabilities capabilities = this.capabilities;
+        if (capabilities == null) {
+            logger.debug("The 'shadeCapabilities' field has not yet been initialized");
+            updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
+            updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
+            updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
             return;
         }
-        updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
-        updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
-        updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
+        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));
     }
 
-    private void updateBatteryLevel(int batteryStatus) {
+    private void updateBatteryLevelStates(int batteryStatus) {
         int mappedValue;
         switch (batteryStatus) {
             case 1: // Low
@@ -340,45 +416,60 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
     }
 
-    private void moveShade(CoordinateSystem coordSys, int newPercent) {
-        try {
-            HDPowerViewHubHandler bridge;
-            if ((bridge = getBridgeHandler()) == null) {
-                throw new HubProcessingException("Missing bridge handler");
-            }
-            HDPowerViewWebTargets webTargets = bridge.getWebTargets();
-            if (webTargets == null) {
-                throw new HubProcessingException("Web targets not initialized");
+    private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
+            throws HubProcessingException, HubMaintenanceException {
+        ShadePosition newPosition = null;
+        // (try to) read the positions from the hub
+        Shade shade = webTargets.getShade(shadeId);
+        if (shade != null) {
+            ShadeData shadeData = shade.shade;
+            if (shadeData != null) {
+                updateCapabilities(shadeData);
+                newPosition = shadeData.positions;
             }
-            ShadePosition newPosition = null;
-            // (try to) read the positions from the hub
-            int shadeId = getShadeId();
-            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());
+        }
+        // if no positions returned, then create a new position
+        if (newPosition == null) {
+            newPosition = new ShadePosition();
+        }
+        Capabilities capabilities = getCapabilitiesOrDefault();
+        // set the new position value, and write the positions to the hub
+        shade = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
+        if (shade != null) {
+            updateShadePositions(shade);
+        }
+    }
+
+    private void stopShade(HDPowerViewWebTargets webTargets, int shadeId)
+            throws HubProcessingException, HubMaintenanceException {
+        Shade shade = webTargets.stopShade(shadeId);
+        if (shade != null) {
+            updateShadePositions(shade);
+        }
+        // Positions in response from stop motion is not updated to to actual positions yet,
+        // so we need to request hard refresh.
+        requestRefreshShadePosition();
+    }
+
+    private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId)
+            throws HubProcessingException, HubMaintenanceException {
+        Shade shade = webTargets.calibrateShade(shadeId);
+        if (shade != null) {
+            updateShadePositions(shade);
+        }
+    }
+
+    private void updateShadePositions(Shade shade) {
+        ShadeData shadeData = shade.shade;
+        if (shadeData == null) {
             return;
-        } catch (HubMaintenanceException e) {
-            // exceptions are logged in HDPowerViewWebTargets
+        }
+        ShadePosition shadePosition = shadeData.positions;
+        if (shadePosition == null) {
             return;
         }
+        updateCapabilities(shadeData);
+        updatePositionStates(shadePosition);
     }
 
     private int getShadeId() throws NumberFormatException {
@@ -389,35 +480,12 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
         return Integer.parseInt(str);
     }
 
-    private void stopShade() {
-        try {
-            HDPowerViewHubHandler bridge;
-            if ((bridge = getBridgeHandler()) == null) {
-                throw new HubProcessingException("Missing bridge handler");
-            }
-            HDPowerViewWebTargets webTargets = bridge.getWebTargets();
-            if (webTargets == null) {
-                throw new HubProcessingException("Web targets not initialized");
-            }
-            int shadeId = getShadeId();
-            webTargets.stopShade(shadeId);
-            requestRefreshShadePosition();
-        } catch (HubProcessingException | NumberFormatException e) {
-            logger.warn("Unexpected error: {}", e.getMessage());
-            return;
-        } catch (HubMaintenanceException e) {
-            // exceptions are logged in HDPowerViewWebTargets
-            return;
-        }
-    }
-
     /**
      * Request that the shade shall undergo a 'hard' refresh for querying its current position
      */
     protected synchronized void requestRefreshShadePosition() {
         if (refreshPositionFuture == null) {
-            refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
-                    TimeUnit.SECONDS);
+            refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
         }
     }
 
@@ -426,7 +494,7 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
      */
     protected synchronized void requestRefreshShadeSurvey() {
         if (refreshSignalFuture == null) {
-            refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, REFRESH_DELAY_SEC, TimeUnit.SECONDS);
+            refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
         }
     }
 
@@ -435,8 +503,7 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
      */
     protected synchronized void requestRefreshShadeBatteryLevel() {
         if (refreshBatteryLevelFuture == null) {
-            refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
-                    TimeUnit.SECONDS);
+            refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
         }
     }
 
@@ -465,7 +532,6 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
             if (webTargets == null) {
                 throw new HubProcessingException("Web targets not initialized");
             }
-            int shadeId = getShadeId();
             Shade shade;
             switch (kind) {
                 case POSITION:
@@ -491,12 +557,17 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
                     if (Boolean.TRUE.equals(shadeData.timedOut)) {
                         logger.warn("Shade {} wireless refresh time out", shadeId);
                     } else if (kind == RefreshKind.POSITION) {
+                        updateShadePositions(shade);
                         updateHardProperties(shadeData);
                     }
                 }
             }
-        } catch (HubProcessingException | NumberFormatException e) {
-            logger.warn("Unexpected error: {}", e.getMessage());
+        } catch (HubProcessingException e) {
+            // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
+            // for any ongoing requests. Logging this would only cause confusion.
+            if (!isDisposing) {
+                logger.warn("Unexpected error: {}", e.getMessage());
+            }
         } catch (HubMaintenanceException e) {
             // exceptions are logged in HDPowerViewWebTargets
         }
index 8c0876b6366dd93173ddef25e44e2e0ae03a642f..4a6ec1312e3d61249c75b9c8dbd88ee41263512c 100644 (file)
@@ -29,6 +29,8 @@ thing-type.config.hdpowerview.shade.id.description = The numeric ID of the Power
 
 channel-type.hdpowerview.battery-voltage.label = Battery Voltage
 channel-type.hdpowerview.battery-voltage.description = Battery voltage reported by the shade
+channel-type.hdpowerview.shade-calibrate.label = Calibrate
+channel-type.hdpowerview.shade-calibrate.description = Perform calibration of the shade
 channel-type.hdpowerview.shade-position.label = Position
 channel-type.hdpowerview.shade-position.description = The vertical position of the shade
 channel-type.hdpowerview.shade-vane.label = Vane
index 8e4cc60693d2bbb3c03f88c6b34d257800802c25..80b128b60278bcfd7542437667ec692957e68da4 100644 (file)
@@ -60,6 +60,7 @@
                                <description>The secondary vertical position (on top-down/bottom-up shades)</description>
                        </channel>
                        <channel id="vane" typeId="shade-vane"/>
+                       <channel id="calibrate" typeId="shade-calibrate"/>
                        <channel id="lowBattery" typeId="system.low-battery"/>
                        <channel id="batteryLevel" typeId="system.battery-level"/>
                        <channel id="batteryVoltage" typeId="battery-voltage"/>
                <description>The opening of the slats in the shade</description>
        </channel-type>
 
+       <channel-type id="shade-calibrate" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Calibrate</label>
+               <description>Perform calibration of the shade</description>
+       </channel-type>
+
        <channel-type id="scene-activate">
                <item-type>Switch</item-type>
                <label>Activate</label>