diff --git a/CODEOWNERS b/CODEOWNERS index 0f1b2fe4d25..ae972027ead 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,7 +116,7 @@ /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 diff --git a/bundles/org.openhab.binding.hdpowerview/README.md b/bundles/org.openhab.binding.hdpowerview/README.md index 1c70a3a68f6..46f0e27e892 100644 --- a/bundles/org.openhab.binding.hdpowerview/README.md +++ b/bundles/org.openhab.binding.hdpowerview/README.md @@ -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: 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 2cc89626191..371f63d7176 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 @@ -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"; 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 d7b3c1eaa81..deabb7fb777 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 @@ -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/ShadeIdStop.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java similarity index 64% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdStop.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java index 660644038a6..e1ec80e4874 100644 --- 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/ShadeCalibrate.java @@ -13,21 +13,18 @@ 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 + * A request to calibrate a shade * - * @author Andrew Fiddian-Green - Initial contribution + * @author Jacob Laursen - Initial contribution */ @NonNullByDefault -class ShadeIdStop { +public class ShadeCalibrate { - int id; - public @Nullable String motion; + public ShadeMotion shade; - public ShadeIdStop(int id) { - this.id = id; - this.motion = "stop"; + 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/ShadeMotion.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java new file mode 100644 index 00000000000..f4c9d784a5a --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java index c70d31cd115..fe688f4f75b 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java @@ -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/ShadeIdPosition.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java similarity index 77% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeIdPosition.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java index 644d45fabb7..1c544c047e8 100644 --- 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/ShadePositions.java @@ -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; /** @@ -22,13 +21,11 @@ import org.openhab.binding.hdpowerview.internal.api.ShadePosition; * @author Andy Lintner - Initial contribution */ @NonNullByDefault -class ShadeIdPosition { +class ShadePositions { - int id; - public @Nullable ShadePosition positions; + public ShadePosition positions; - public ShadeIdPosition(int id, ShadePosition position) { - this.id = id; + public ShadePositions(ShadePosition position) { this.positions = position; } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java index 98a183f70c4..ed7ab4c4b0a 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java @@ -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); } } 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 index 0a1cf903abf..7326b44710b 100644 --- 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 @@ -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; } 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 f3b2705c1d2..e5e9f7ceee9 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 @@ -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 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)); + if (positions == null) { return; } - updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF); - updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF); - updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF); + Capabilities capabilities = getCapabilitiesOrDefault(); + final Map 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 '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 updateBatteryLevel(int batteryStatus) { + 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, 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 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"); + 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; } - HDPowerViewWebTargets webTargets = bridge.getWebTargets(); - if (webTargets == null) { - throw new HubProcessingException("Web targets not initialized"); - } - 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()); - return; - } catch (HubMaintenanceException e) { - // exceptions are logged in HDPowerViewWebTargets + } + // 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; } + 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 } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties index 8c0876b6366..4a6ec1312e3 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties +++ b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties @@ -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 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 8e4cc60693d..80b128b6027 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 @@ -60,6 +60,7 @@ The secondary vertical position (on top-down/bottom-up shades) + @@ -96,6 +97,12 @@ The opening of the slats in the shade + + Switch + + Perform calibration of the shade + + Switch