[hdpowerview] Add support for calibrating a shade (#12002)

* 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>
This commit is contained in:
Jacob Laursen 2022-01-14 22:59:01 +01:00 committed by GitHub
parent 843ca55c68
commit 2832567fc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 319 additions and 176 deletions

View File

@ -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

View 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:

View 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";

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View 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);
}
}

View 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;
/**
@ -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;
}
}

View 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);
}
}

View 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;
}

View 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);
}
}
// 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<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 '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
}

View 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

View 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"/>
@ -96,6 +97,12 @@
<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>