2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hdpowerview.internal.handler;
15 import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*;
16 import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import javax.ws.rs.NotSupportedException;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
27 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
28 import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
29 import org.openhab.binding.hdpowerview.internal.api.Firmware;
30 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
31 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
33 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
34 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
35 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
36 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
37 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
38 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
39 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
40 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.PercentType;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.types.StopMoveType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.types.UpDownType;
48 import org.openhab.core.library.unit.Units;
49 import org.openhab.core.thing.Bridge;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.types.Command;
55 import org.openhab.core.types.RefreshType;
56 import org.openhab.core.types.UnDefType;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
61 * Handles commands for an HD PowerView Shade
63 * @author Andy Lintner - Initial contribution
64 * @author Andrew Fiddian-Green - Added support for secondary rail positions
67 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
69 private enum RefreshKind {
75 private static final String COMMAND_CALIBRATE = "CALIBRATE";
76 private static final String COMMAND_IDENTIFY = "IDENTIFY";
78 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
79 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
81 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
82 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
83 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
84 private @Nullable Capabilities capabilities;
86 private boolean isDisposing;
88 public HDPowerViewShadeHandler(Thing thing) {
93 public void initialize() {
95 shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
96 logger.debug("Initializing shade handler for shade {}", shadeId);
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
99 "@text/offline.conf-error.invalid-id");
102 Bridge bridge = getBridge();
103 if (bridge == null) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
107 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
109 "@text/offline.conf-error.invalid-bridge-handler");
112 ThingStatus bridgeStatus = bridge.getStatus();
113 if (bridgeStatus == ThingStatus.ONLINE) {
114 updateStatus(ThingStatus.UNKNOWN);
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
121 public void dispose() {
122 logger.debug("Disposing shade handler for shade {}", shadeId);
124 ScheduledFuture<?> future = refreshPositionFuture;
125 if (future != null) {
128 refreshPositionFuture = null;
129 future = refreshSignalFuture;
130 if (future != null) {
133 refreshSignalFuture = null;
134 future = refreshBatteryLevelFuture;
135 if (future != null) {
138 refreshBatteryLevelFuture = null;
143 public void handleCommand(ChannelUID channelUID, Command command) {
144 String channelId = channelUID.getId();
146 if (RefreshType.REFRESH == command) {
148 case CHANNEL_SHADE_POSITION:
149 case CHANNEL_SHADE_SECONDARY_POSITION:
150 case CHANNEL_SHADE_VANE:
151 requestRefreshShadePosition();
153 case CHANNEL_SHADE_LOW_BATTERY:
154 case CHANNEL_SHADE_BATTERY_LEVEL:
155 case CHANNEL_SHADE_BATTERY_VOLTAGE:
156 requestRefreshShadeBatteryLevel();
158 case CHANNEL_SHADE_SIGNAL_STRENGTH:
159 requestRefreshShadeSurvey();
165 HDPowerViewHubHandler bridge = getBridgeHandler();
166 if (bridge == null) {
167 logger.warn("Missing bridge handler");
170 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
171 if (webTargets == null) {
172 logger.warn("Web targets not initialized");
176 handleShadeCommand(channelId, command, webTargets, shadeId);
177 } catch (HubInvalidResponseException e) {
178 Throwable cause = e.getCause();
180 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
182 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
184 } catch (HubMaintenanceException e) {
185 // exceptions are logged in HDPowerViewWebTargets
186 } catch (HubShadeTimeoutException e) {
187 logger.warn("Shade {} timeout when sending command {}", shadeId, command);
188 } catch (HubException e) {
189 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
190 // for any ongoing requests. Logging this would only cause confusion.
192 logger.warn("Unexpected error: {}", e.getMessage());
197 private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
198 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
199 HubShadeTimeoutException {
201 case CHANNEL_SHADE_POSITION:
202 if (command instanceof PercentType) {
203 moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
204 } else if (command instanceof UpDownType) {
205 moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
206 } else if (command instanceof StopMoveType) {
207 if (StopMoveType.STOP == command) {
208 stopShade(webTargets, shadeId);
210 logger.warn("Unexpected StopMoveType command");
215 case CHANNEL_SHADE_VANE:
216 if (command instanceof PercentType) {
217 moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
218 } else if (command instanceof OnOffType) {
219 moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
223 case CHANNEL_SHADE_SECONDARY_POSITION:
224 if (command instanceof PercentType) {
225 moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
226 } else if (command instanceof UpDownType) {
227 moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
228 } else if (command instanceof StopMoveType) {
229 if (StopMoveType.STOP == command) {
230 stopShade(webTargets, shadeId);
232 logger.warn("Unexpected StopMoveType command");
237 case CHANNEL_SHADE_COMMAND:
238 if (command instanceof StringType) {
239 if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
240 logger.debug("Identify shade {}", shadeId);
241 identifyShade(webTargets, shadeId);
242 } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
243 logger.debug("Calibrate shade {}", shadeId);
244 calibrateShade(webTargets, shadeId);
247 logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
254 * Update the state of the channels based on the ShadeData provided.
256 * @param shadeData the ShadeData to be used.
258 protected void onReceiveUpdate(ShadeData shadeData) {
259 updateStatus(ThingStatus.ONLINE);
260 updateCapabilities(shadeData);
261 updateSoftProperties(shadeData);
262 updateFirmwareProperties(shadeData);
263 ShadePosition shadePosition = shadeData.positions;
264 if (shadePosition != null) {
265 updatePositionStates(shadePosition);
267 updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
268 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
271 private void updateCapabilities(ShadeData shade) {
272 if (capabilities != null) {
276 Integer value = shade.capabilities;
278 int valueAsInt = value.intValue();
279 logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
280 capabilities = db.getCapabilities(valueAsInt);
282 logger.debug("Capabilities not included in shade response");
286 private Capabilities getCapabilitiesOrDefault() {
287 Capabilities capabilities = this.capabilities;
288 if (capabilities == null) {
289 return new Capabilities();
295 * Update the Thing's properties based on the contents of the provided ShadeData.
297 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
298 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
302 private void updateSoftProperties(ShadeData shadeData) {
303 final Map<String, String> properties = getThing().getProperties();
304 boolean propChanged = false;
306 // update 'type' property
307 final int type = shadeData.type;
308 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
309 String propOldVal = properties.getOrDefault(propKey, "");
310 String propNewVal = db.getType(type).toString();
311 if (!propNewVal.equals(propOldVal)) {
313 getThing().setProperty(propKey, propNewVal);
314 if ((type > 0) && !db.isTypeInDatabase(type)) {
315 db.logTypeNotInDatabase(type);
319 // update 'capabilities' property
320 final Integer temp = shadeData.capabilities;
321 final int capabilitiesVal = temp != null ? temp.intValue() : -1;
322 Capabilities capabilities = db.getCapabilities(capabilitiesVal);
323 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
324 propOldVal = properties.getOrDefault(propKey, "");
325 propNewVal = capabilities.toString();
326 if (!propNewVal.equals(propOldVal)) {
328 getThing().setProperty(propKey, propNewVal);
329 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
330 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
334 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
335 && (capabilitiesVal != db.getType(type).getCapabilities())) {
336 db.logCapabilitiesMismatch(type, capabilitiesVal);
340 private void updateFirmwareProperties(ShadeData shadeData) {
341 Map<String, String> properties = editProperties();
342 Firmware shadeFirmware = shadeData.firmware;
343 Firmware motorFirmware = shadeData.motor;
344 if (shadeFirmware != null) {
345 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
347 if (motorFirmware != null) {
348 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
350 updateProperties(properties);
354 * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
356 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
357 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
358 * potential need to add support for that type resp. capabilities.
362 private void updateHardProperties(ShadeData shadeData) {
363 final ShadePosition positions = shadeData.positions;
364 if (positions == null) {
367 Capabilities capabilities = getCapabilitiesOrDefault();
368 final Map<String, String> properties = getThing().getProperties();
370 // update 'secondary rail detected' property
371 String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
372 String propOldVal = properties.getOrDefault(propKey, "");
373 boolean propNewBool = positions.secondaryRailDetected();
374 String propNewVal = String.valueOf(propNewBool);
375 if (!propNewVal.equals(propOldVal)) {
376 getThing().setProperty(propKey, propNewVal);
377 if (propNewBool != capabilities.supportsSecondary()) {
378 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
382 // update 'tilt anywhere detected' property
383 propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
384 propOldVal = properties.getOrDefault(propKey, "");
385 propNewBool = positions.tiltAnywhereDetected();
386 propNewVal = String.valueOf(propNewBool);
387 if (!propNewVal.equals(propOldVal)) {
388 getThing().setProperty(propKey, propNewVal);
389 if (propNewBool != capabilities.supportsTiltAnywhere()) {
390 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
395 private void updatePositionStates(ShadePosition shadePos) {
396 Capabilities capabilities = this.capabilities;
397 if (capabilities == null) {
398 logger.debug("The 'shadeCapabilities' field has not yet been initialized");
399 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
400 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
401 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
404 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
405 updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
406 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
409 private void updateBatteryStates(int batteryStatus, double batteryStrength) {
410 updateBatteryLevelStates(batteryStatus);
411 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
412 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
415 private void updateBatteryLevelStates(int batteryStatus) {
417 switch (batteryStatus) {
425 case 4: // Plugged in
428 default: // No status available (0) or invalid
429 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
430 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
433 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
434 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
437 private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
438 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
439 HubShadeTimeoutException {
440 ShadePosition newPosition = null;
441 // (try to) read the positions from the hub
442 ShadeData shadeData = webTargets.getShade(shadeId);
443 updateCapabilities(shadeData);
444 newPosition = shadeData.positions;
445 // if no positions returned, then create a new position
446 if (newPosition == null) {
447 newPosition = new ShadePosition();
449 Capabilities capabilities = getCapabilitiesOrDefault();
450 // set the new position value, and write the positions to the hub
451 shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
452 updateShadePositions(shadeData);
455 private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
456 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
457 updateShadePositions(webTargets.stopShade(shadeId));
458 // Positions in response from stop motion is not updated to to actual positions yet,
459 // so we need to request hard refresh.
460 requestRefreshShadePosition();
463 private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
464 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
465 updateShadePositions(webTargets.jogShade(shadeId));
468 private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
469 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
470 updateShadePositions(webTargets.calibrateShade(shadeId));
473 private void updateShadePositions(ShadeData shadeData) {
474 ShadePosition shadePosition = shadeData.positions;
475 if (shadePosition == null) {
478 updateCapabilities(shadeData);
479 updatePositionStates(shadePosition);
483 * Request that the shade shall undergo a 'hard' refresh for querying its current position
485 protected synchronized void requestRefreshShadePosition() {
486 if (refreshPositionFuture == null) {
487 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
492 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
494 protected synchronized void requestRefreshShadeSurvey() {
495 if (refreshSignalFuture == null) {
496 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
501 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
503 protected synchronized void requestRefreshShadeBatteryLevel() {
504 if (refreshBatteryLevelFuture == null) {
505 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
509 private void doRefreshShadePosition() {
510 this.doRefreshShade(RefreshKind.POSITION);
511 refreshPositionFuture = null;
514 private void doRefreshShadeSignal() {
515 this.doRefreshShade(RefreshKind.SURVEY);
516 refreshSignalFuture = null;
519 private void doRefreshShadeBatteryLevel() {
520 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
521 refreshBatteryLevelFuture = null;
524 private void doRefreshShade(RefreshKind kind) {
526 HDPowerViewHubHandler bridge;
527 if ((bridge = getBridgeHandler()) == null) {
528 throw new HubProcessingException("Missing bridge handler");
530 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
531 if (webTargets == null) {
532 throw new HubProcessingException("Web targets not initialized");
537 shadeData = webTargets.refreshShadePosition(shadeId);
538 updateShadePositions(shadeData);
539 updateHardProperties(shadeData);
542 Survey survey = webTargets.getShadeSurvey(shadeId);
543 if (survey.surveyData != null) {
544 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
546 logger.warn("No response from shade {} survey", shadeId);
550 shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
551 updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
554 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
556 } catch (HubInvalidResponseException e) {
557 Throwable cause = e.getCause();
559 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
561 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
563 } catch (HubMaintenanceException e) {
564 // exceptions are logged in HDPowerViewWebTargets
565 } catch (HubShadeTimeoutException e) {
566 logger.info("Shade {} wireless refresh time out", shadeId);
567 } catch (HubException e) {
568 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
569 // for any ongoing requests. Logging this would only cause confusion.
571 logger.warn("Unexpected error: {}", e.getMessage());