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 99c96eded..aa7dd2f3c 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 @@ -50,6 +50,7 @@ import org.openhab.binding.hdpowerview.internal.api.responses.Survey; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException; +import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -208,15 +209,16 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ - public ShadeData moveShade(int shadeId, ShadePosition position) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + public ShadeData moveShade(int shadeId, ShadePosition position) throws HubInvalidResponseException, + HubProcessingException, HubMaintenanceException, HubShadeTimeoutException { String jsonRequest = gson.toJson(new ShadeMove(position)); String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest); return shadeDataFromJson(jsonResponse); } - private ShadeData shadeDataFromJson(String json) throws HubInvalidResponseException { + private ShadeData shadeDataFromJson(String json) throws HubInvalidResponseException, HubShadeTimeoutException { try { Shade shade = gson.fromJson(json, Shade.class); if (shade == null) { @@ -226,6 +228,9 @@ public class HDPowerViewWebTargets { if (shadeData == null) { throw new HubInvalidResponseException("Missing 'shade.shade' element"); } + if (Boolean.TRUE.equals(shadeData.timedOut)) { + throw new HubShadeTimeoutException("Timeout when sending request to the shade"); + } return shadeData; } catch (JsonParseException e) { throw new HubInvalidResponseException("Error parsing shade response", e); @@ -240,9 +245,10 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ - public ShadeData stopShade(int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + public ShadeData stopShade(int shadeId) throws HubInvalidResponseException, HubProcessingException, + HubMaintenanceException, HubShadeTimeoutException { String jsonRequest = gson.toJson(new ShadeStop()); String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest); return shadeDataFromJson(jsonResponse); @@ -256,9 +262,10 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ - public ShadeData jogShade(int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + public ShadeData jogShade(int shadeId) throws HubInvalidResponseException, HubProcessingException, + HubMaintenanceException, HubShadeTimeoutException { String jsonRequest = gson.toJson(new ShadeJog()); String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest); return shadeDataFromJson(jsonResponse); @@ -272,9 +279,10 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ - public ShadeData calibrateShade(int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + public ShadeData calibrateShade(int shadeId) throws HubInvalidResponseException, HubProcessingException, + HubMaintenanceException, HubShadeTimeoutException { String jsonRequest = gson.toJson(new ShadeCalibrate()); String jsonResponse = invoke(HttpMethod.PUT, shades + Integer.toString(shadeId), null, jsonRequest); return shadeDataFromJson(jsonResponse); @@ -574,9 +582,10 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ - public ShadeData getShade(int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + public ShadeData getShade(int shadeId) throws HubInvalidResponseException, HubProcessingException, + HubMaintenanceException, HubShadeTimeoutException { String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), null, null); return shadeDataFromJson(jsonResponse); } @@ -591,9 +600,10 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ public ShadeData refreshShadePosition(int shadeId) - throws JsonParseException, HubProcessingException, HubMaintenanceException { + throws JsonParseException, HubProcessingException, HubMaintenanceException, HubShadeTimeoutException { String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), Query.of("refresh", Boolean.toString(true)), null); return shadeDataFromJson(jsonResponse); @@ -636,9 +646,10 @@ public class HDPowerViewWebTargets { * @throws HubInvalidResponseException if response is invalid * @throws HubProcessingException if there is any processing error * @throws HubMaintenanceException if the hub is down for maintenance + * @throws HubShadeTimeoutException if the shade did not respond to a request */ - public ShadeData refreshShadeBatteryLevel(int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + public ShadeData refreshShadeBatteryLevel(int shadeId) throws HubInvalidResponseException, HubProcessingException, + HubMaintenanceException, HubShadeTimeoutException { String jsonResponse = invoke(HttpMethod.GET, shades + Integer.toString(shadeId), Query.of("updateBatteryLevel", Boolean.toString(true)), null); return shadeDataFromJson(jsonResponse); diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/exceptions/HubShadeTimeoutException.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/exceptions/HubShadeTimeoutException.java new file mode 100644 index 000000000..3dcedc684 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/exceptions/HubShadeTimeoutException.java @@ -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.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link HubShadeTimeoutException} is a custom exception for the HD PowerView Hub + * which is thrown when a shade does not respond to a request. + * + * @author @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class HubShadeTimeoutException extends HubException { + + private static final long serialVersionUID = -362347489903471011L; + + public HubShadeTimeoutException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java index 3f6d17475..cfea061f2 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java @@ -58,6 +58,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; @@ -204,7 +205,11 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { if (childHandler instanceof HDPowerViewShadeHandler) { ShadeData shadeData = pendingShadeInitializations.remove(childThing.getUID()); if (shadeData != null) { - updateShadeThing(shadeData.id, childThing, shadeData); + if (shadeData.id > 0) { + updateShadeThing(shadeData.id, childThing, shadeData); + } else { + updateUnknownShadeThing(childThing); + } } } super.childHandlerInitialized(childHandler, childThing); @@ -354,15 +359,15 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { Thing thing = item.getKey(); int shadeId = item.getValue(); ShadeData shadeData = idShadeDataMap.get(shadeId); - updateShadeThing(shadeId, thing, shadeData); + if (shadeData != null) { + updateShadeThing(shadeId, thing, shadeData); + } else { + updateUnknownShadeThing(thing); + } } } - private void updateShadeThing(int shadeId, Thing thing, @Nullable ShadeData shadeData) { - if (shadeData == null) { - logger.debug("Shade '{}' has no data in hub", shadeId); - return; - } + private void updateShadeThing(int shadeId, Thing thing, ShadeData shadeData) { HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler()); if (thingHandler == null) { logger.debug("Shade '{}' handler not initialized", shadeId); @@ -390,6 +395,36 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { } } + private void updateUnknownShadeThing(Thing thing) { + String shadeId = thing.getUID().getId(); + logger.debug("Shade '{}' has no data in hub", shadeId); + HDPowerViewShadeHandler thingHandler = ((HDPowerViewShadeHandler) thing.getHandler()); + if (thingHandler == null) { + logger.debug("Shade '{}' handler not initialized", shadeId); + pendingShadeInitializations.put(thing.getUID(), new ShadeData()); + return; + } + ThingStatus thingStatus = thingHandler.getThing().getStatus(); + switch (thingStatus) { + case UNKNOWN: + case ONLINE: + case OFFLINE: + thing.setStatusInfo(new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.GONE, + "@text/offline.gone.shade-unknown-to-hub")); + break; + case UNINITIALIZED: + case INITIALIZING: + logger.debug("Shade '{}' handler not yet ready; status: {}", shadeId, thingStatus); + pendingShadeInitializations.put(thing.getUID(), new ShadeData()); + break; + case REMOVING: + case REMOVED: + default: + logger.debug("Ignoring shade status update for shade '{}' in status {}", shadeId, thingStatus); + break; + } + } + private List fetchScenes() throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { HDPowerViewWebTargets webTargets = this.webTargets; @@ -601,6 +636,11 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { Map thingIdMap = getShadeThingIdMap(); for (Entry item : thingIdMap.entrySet()) { Thing thing = item.getKey(); + if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.GONE) { + // Skip shades unknown to the Hub. + logger.debug("Shade '{}' is unknown, skipping position refresh", item.getValue()); + continue; + } ThingHandler handler = thing.getHandler(); if (handler instanceof HDPowerViewShadeHandler) { ((HDPowerViewShadeHandler) handler).requestRefreshShadePosition(); @@ -615,6 +655,11 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { Map thingIdMap = getShadeThingIdMap(); for (Entry item : thingIdMap.entrySet()) { Thing thing = item.getKey(); + if (thing.getStatusInfo().getStatusDetail() == ThingStatusDetail.GONE) { + // Skip shades unknown to the Hub. + logger.debug("Shade '{}' is unknown, skipping battery level refresh", item.getValue()); + continue; + } ThingHandler handler = thing.getHandler(); if (handler instanceof HDPowerViewShadeHandler) { ((HDPowerViewShadeHandler) handler).requestRefreshShadeBatteryLevel(); 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 8118dfd39..5b0cb829a 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 @@ -37,6 +37,7 @@ import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException; +import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; @@ -177,6 +178,8 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } } catch (HubMaintenanceException e) { // exceptions are logged in HDPowerViewWebTargets + } catch (HubShadeTimeoutException e) { + logger.warn("Shade {} timeout when sending command {}", shadeId, command); } catch (HubException e) { // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke() // for any ongoing requests. Logging this would only cause confusion. @@ -187,7 +190,8 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException, + HubShadeTimeoutException { switch (channelId) { case CHANNEL_SHADE_POSITION: if (command instanceof PercentType) { @@ -244,26 +248,19 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { /** * Update the state of the channels based on the ShadeData provided. * - * @param shadeData the ShadeData to be used; may be null. + * @param shadeData the ShadeData to be used. */ - protected void onReceiveUpdate(@Nullable ShadeData shadeData) { - if (shadeData != null) { - updateStatus(ThingStatus.ONLINE); - updateCapabilities(shadeData); - updateSoftProperties(shadeData); - updateFirmwareProperties(shadeData); - 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); - updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength)); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + protected void onReceiveUpdate(ShadeData shadeData) { + updateStatus(ThingStatus.ONLINE); + updateCapabilities(shadeData); + updateSoftProperties(shadeData); + updateFirmwareProperties(shadeData); + ShadePosition shadePosition = shadeData.positions; + if (shadePosition != null) { + updatePositionStates(shadePosition); } + updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength); + updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength)); } private void updateCapabilities(ShadeData shade) { @@ -404,6 +401,12 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION)); } + private void updateBatteryStates(int batteryStatus, double batteryStrength) { + updateBatteryLevelStates(batteryStatus); + updateState(CHANNEL_SHADE_BATTERY_VOLTAGE, + batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF); + } + private void updateBatteryLevelStates(int batteryStatus) { int mappedValue; switch (batteryStatus) { @@ -427,7 +430,8 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException, + HubShadeTimeoutException { ShadePosition newPosition = null; // (try to) read the positions from the hub ShadeData shadeData = webTargets.getShade(shadeId); @@ -443,21 +447,21 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { updateShadePositions(shadeData); } - private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException, + HubProcessingException, HubMaintenanceException, HubShadeTimeoutException { updateShadePositions(webTargets.stopShade(shadeId)); // Positions in response from stop motion is not updated to to actual positions yet, // so we need to request hard refresh. requestRefreshShadePosition(); } - private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException, + HubProcessingException, HubMaintenanceException, HubShadeTimeoutException { updateShadePositions(webTargets.jogShade(shadeId)); } - private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) - throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException { + private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException, + HubProcessingException, HubMaintenanceException, HubShadeTimeoutException { updateShadePositions(webTargets.calibrateShade(shadeId)); } @@ -526,6 +530,8 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { switch (kind) { case POSITION: shadeData = webTargets.refreshShadePosition(shadeId); + updateShadePositions(shadeData); + updateHardProperties(shadeData); break; case SURVEY: Survey survey = webTargets.getShadeSurvey(shadeId); @@ -534,19 +540,14 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } else { logger.warn("No response from shade {} survey", shadeId); } - return; + break; case BATTERY_LEVEL: shadeData = webTargets.refreshShadeBatteryLevel(shadeId); + updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength); break; default: throw new NotSupportedException("Unsupported refresh kind " + kind.toString()); } - if (Boolean.TRUE.equals(shadeData.timedOut)) { - logger.warn("Shade {} wireless refresh time out", shadeId); - } else if (kind == RefreshKind.POSITION) { - updateShadePositions(shadeData); - updateHardProperties(shadeData); - } } catch (HubInvalidResponseException e) { Throwable cause = e.getCause(); if (cause == null) { @@ -556,6 +557,8 @@ public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler { } } catch (HubMaintenanceException e) { // exceptions are logged in HDPowerViewWebTargets + } catch (HubShadeTimeoutException e) { + logger.info("Shade {} wireless refresh time out", shadeId); } catch (HubException e) { // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke() // for any ongoing requests. Logging this would only cause confusion. 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 ef907e926..d09d2fd98 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 @@ -52,6 +52,7 @@ channel-type.hdpowerview.shade-vane.description = The opening of the slats in th offline.conf-error.no-host-address = Host address must be set offline.conf-error.invalid-id = Configuration 'id' not a valid integer offline.conf-error.invalid-bridge-handler = Invalid bridge handler +offline.gone.shade-unknown-to-hub = Shade is unknown to Hub # dynamic channels