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.*;
18 import java.util.HashMap;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import javax.ws.rs.NotSupportedException;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
28 import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
29 import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
30 import org.openhab.binding.hdpowerview.internal.api.Firmware;
31 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
34 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
35 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
36 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
37 import org.openhab.binding.hdpowerview.internal.exceptions.HubException;
38 import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException;
39 import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException;
40 import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException;
41 import org.openhab.binding.hdpowerview.internal.exceptions.HubShadeTimeoutException;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.library.types.PercentType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StopMoveType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.types.UpDownType;
49 import org.openhab.core.library.unit.Units;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.RefreshType;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
62 * Handles commands for an HD PowerView Shade
64 * @author Andy Lintner - Initial contribution
65 * @author Andrew Fiddian-Green - Added support for secondary rail positions
68 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
70 private enum RefreshKind {
76 private static final String COMMAND_CALIBRATE = "CALIBRATE";
77 private static final String COMMAND_IDENTIFY = "IDENTIFY";
79 private static final String DETECTED_SECONDARY_RAIL = "secondaryRailDetected";
80 private static final String DETECTED_TILT_ANYWHERE = "tiltAnywhereDetected";
81 private final Map<String, String> detectedCapabilities = new HashMap<>();
83 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
84 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
86 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
87 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
88 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
89 private @Nullable Capabilities capabilities;
91 private boolean isDisposing;
93 public HDPowerViewShadeHandler(Thing thing) {
98 public void initialize() {
100 shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
101 logger.debug("Initializing shade handler for shade {}", shadeId);
102 Bridge bridge = getBridge();
103 if (bridge == null) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
105 "@text/offline.conf-error.invalid-bridge-handler");
109 updateStatus(ThingStatus.UNKNOWN);
113 public void dispose() {
114 logger.debug("Disposing shade handler for shade {}", shadeId);
116 ScheduledFuture<?> future = refreshPositionFuture;
117 if (future != null) {
120 refreshPositionFuture = null;
121 future = refreshSignalFuture;
122 if (future != null) {
125 refreshSignalFuture = null;
126 future = refreshBatteryLevelFuture;
127 if (future != null) {
130 refreshBatteryLevelFuture = null;
135 public void handleCommand(ChannelUID channelUID, Command command) {
136 String channelId = channelUID.getId();
138 if (RefreshType.REFRESH == command) {
140 case CHANNEL_SHADE_POSITION:
141 case CHANNEL_SHADE_SECONDARY_POSITION:
142 case CHANNEL_SHADE_VANE:
143 requestRefreshShadePosition();
145 case CHANNEL_SHADE_LOW_BATTERY:
146 case CHANNEL_SHADE_BATTERY_LEVEL:
147 case CHANNEL_SHADE_BATTERY_VOLTAGE:
148 requestRefreshShadeBatteryLevel();
150 case CHANNEL_SHADE_SIGNAL_STRENGTH:
151 requestRefreshShadeSurvey();
157 HDPowerViewHubHandler bridge = getBridgeHandler();
158 if (bridge == null) {
159 logger.warn("Missing bridge handler");
162 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
163 if (webTargets == null) {
164 logger.warn("Web targets not initialized");
168 handleShadeCommand(channelId, command, webTargets, shadeId);
169 } catch (HubInvalidResponseException e) {
170 Throwable cause = e.getCause();
172 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
174 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
176 } catch (HubMaintenanceException e) {
177 // exceptions are logged in HDPowerViewWebTargets
178 } catch (HubShadeTimeoutException e) {
179 logger.warn("Shade {} timeout when sending command {}", shadeId, command);
180 } catch (HubException e) {
181 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
182 // for any ongoing requests. Logging this would only cause confusion.
184 logger.warn("Unexpected error: {}", e.getMessage());
189 private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
190 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
191 HubShadeTimeoutException {
193 case CHANNEL_SHADE_POSITION:
194 if (command instanceof PercentType) {
195 moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
196 } else if (command instanceof UpDownType) {
197 moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
198 } else if (command instanceof StopMoveType) {
199 if (StopMoveType.STOP == command) {
200 stopShade(webTargets, shadeId);
202 logger.warn("Unexpected StopMoveType command");
207 case CHANNEL_SHADE_VANE:
208 if (command instanceof PercentType) {
209 moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
210 } else if (command instanceof OnOffType) {
211 moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
215 case CHANNEL_SHADE_SECONDARY_POSITION:
216 if (command instanceof PercentType) {
217 moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
218 } else if (command instanceof UpDownType) {
219 moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
220 } else if (command instanceof StopMoveType) {
221 if (StopMoveType.STOP == command) {
222 stopShade(webTargets, shadeId);
224 logger.warn("Unexpected StopMoveType command");
229 case CHANNEL_SHADE_COMMAND:
230 if (command instanceof StringType) {
231 if (COMMAND_IDENTIFY.equals(((StringType) command).toString())) {
232 logger.debug("Identify shade {}", shadeId);
233 identifyShade(webTargets, shadeId);
234 } else if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
235 logger.debug("Calibrate shade {}", shadeId);
236 calibrateShade(webTargets, shadeId);
239 logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
246 * Update the state of the channels based on the ShadeData provided.
248 * @param shadeData the ShadeData to be used.
250 protected void onReceiveUpdate(ShadeData shadeData) {
251 updateStatus(ThingStatus.ONLINE);
252 updateCapabilities(shadeData);
253 updateSoftProperties(shadeData);
254 updateFirmwareProperties(shadeData);
255 ShadePosition shadePosition = shadeData.positions;
256 if (shadePosition != null) {
257 updatePositionStates(shadePosition);
259 updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
260 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
263 private void updateCapabilities(ShadeData shade) {
264 if (capabilities != null) {
268 Capabilities capabilities = db.getCapabilities(shade.type, shade.capabilities);
269 if (capabilities.getValue() < 0) {
270 logger.debug("Unable to set capabilities for shade {}", shade.id);
273 logger.debug("Caching capabilities {} for shade {}", capabilities.getValue(), shade.id);
274 this.capabilities = capabilities;
277 private Capabilities getCapabilitiesOrDefault() {
278 Capabilities capabilities = this.capabilities;
279 if (capabilities == null) {
280 return new Capabilities();
286 * Update the Thing's properties based on the contents of the provided ShadeData.
288 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
289 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
293 private void updateSoftProperties(ShadeData shadeData) {
294 final Map<String, String> properties = getThing().getProperties();
295 boolean propChanged = false;
297 // update 'type' property
298 final int type = shadeData.type;
299 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
300 String propOldVal = properties.getOrDefault(propKey, "");
301 String propNewVal = db.getType(type).toString();
302 if (!propNewVal.equals(propOldVal)) {
304 getThing().setProperty(propKey, propNewVal);
305 if ((type > 0) && !db.isTypeInDatabase(type)) {
306 db.logTypeNotInDatabase(type);
310 // update 'capabilities' property
311 Capabilities capabilities = db.getCapabilities(shadeData.capabilities);
312 final int capabilitiesVal = capabilities.getValue();
313 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
314 propOldVal = properties.getOrDefault(propKey, "");
315 propNewVal = capabilities.toString();
316 if (!propNewVal.equals(propOldVal)) {
318 getThing().setProperty(propKey, propNewVal);
319 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
320 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
324 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
325 && (capabilitiesVal != db.getType(type).getCapabilities())) {
326 db.logCapabilitiesMismatch(type, capabilitiesVal);
330 private void updateFirmwareProperties(ShadeData shadeData) {
331 Map<String, String> properties = editProperties();
332 Firmware shadeFirmware = shadeData.firmware;
333 Firmware motorFirmware = shadeData.motor;
334 if (shadeFirmware != null) {
335 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
337 if (motorFirmware != null) {
338 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
340 updateProperties(properties);
344 * After a hard refresh, update the Thing's detected capabilities based on the contents of the provided ShadeData.
346 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
347 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
348 * potential need to add support for that type resp. capabilities.
352 private void updateDetectedCapabilities(ShadeData shadeData) {
353 final ShadePosition positions = shadeData.positions;
354 if (positions == null) {
357 Capabilities capabilities = getCapabilitiesOrDefault();
359 // update 'secondary rail' detected capability
360 String capsKey = DETECTED_SECONDARY_RAIL;
361 String capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
362 boolean capsNewBool = positions.secondaryRailDetected();
363 String capsNewVal = String.valueOf(capsNewBool);
364 if (!capsNewVal.equals(capsOldVal)) {
365 detectedCapabilities.put(capsKey, capsNewVal);
366 if (capsNewBool != capabilities.supportsSecondary()) {
367 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
371 // update 'tilt anywhere' detected capability
372 capsKey = DETECTED_TILT_ANYWHERE;
373 capsOldVal = detectedCapabilities.getOrDefault(capsKey, "");
374 capsNewBool = positions.tiltAnywhereDetected();
375 capsNewVal = String.valueOf(capsNewBool);
376 if (!capsNewVal.equals(capsOldVal)) {
377 detectedCapabilities.put(capsKey, capsNewVal);
378 if (capsNewBool != capabilities.supportsTiltAnywhere()) {
379 db.logPropertyMismatch(capsKey, shadeData.type, capabilities.getValue(), capsNewBool);
384 private void updatePositionStates(ShadePosition shadePos) {
385 Capabilities capabilities = this.capabilities;
386 if (capabilities == null) {
387 logger.debug("The 'shadeCapabilities' field has not yet been initialized");
388 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
389 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
390 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
393 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
394 updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
395 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
398 private void updateBatteryStates(int batteryStatus, double batteryStrength) {
399 updateBatteryLevelStates(batteryStatus);
400 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
401 batteryStrength > 0 ? new QuantityType<>(batteryStrength / 10, Units.VOLT) : UnDefType.UNDEF);
404 private void updateBatteryLevelStates(int batteryStatus) {
406 switch (batteryStatus) {
414 case 4: // Plugged in
417 default: // No status available (0) or invalid
418 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
419 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
422 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
423 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
426 private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
427 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException,
428 HubShadeTimeoutException {
429 ShadePosition newPosition = null;
430 // (try to) read the positions from the hub
431 ShadeData shadeData = webTargets.getShade(shadeId);
432 updateCapabilities(shadeData);
433 newPosition = shadeData.positions;
434 // if no positions returned, then create a new position
435 if (newPosition == null) {
436 newPosition = new ShadePosition();
438 Capabilities capabilities = getCapabilitiesOrDefault();
439 // set the new position value, and write the positions to the hub
440 shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
441 updateShadePositions(shadeData);
444 private void stopShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
445 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
446 updateShadePositions(webTargets.stopShade(shadeId));
447 // Positions in response from stop motion is not updated to to actual positions yet,
448 // so we need to request hard refresh.
449 requestRefreshShadePosition();
452 private void identifyShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
453 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
454 updateShadePositions(webTargets.jogShade(shadeId));
457 private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId) throws HubInvalidResponseException,
458 HubProcessingException, HubMaintenanceException, HubShadeTimeoutException {
459 updateShadePositions(webTargets.calibrateShade(shadeId));
462 private void updateShadePositions(ShadeData shadeData) {
463 ShadePosition shadePosition = shadeData.positions;
464 if (shadePosition == null) {
467 updateCapabilities(shadeData);
468 updatePositionStates(shadePosition);
472 * Request that the shade shall undergo a 'hard' refresh for querying its current position
474 protected synchronized void requestRefreshShadePosition() {
475 if (refreshPositionFuture == null) {
476 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
481 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
483 protected synchronized void requestRefreshShadeSurvey() {
484 if (refreshSignalFuture == null) {
485 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
490 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
492 protected synchronized void requestRefreshShadeBatteryLevel() {
493 if (refreshBatteryLevelFuture == null) {
494 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
498 private void doRefreshShadePosition() {
499 this.doRefreshShade(RefreshKind.POSITION);
500 refreshPositionFuture = null;
503 private void doRefreshShadeSignal() {
504 this.doRefreshShade(RefreshKind.SURVEY);
505 refreshSignalFuture = null;
508 private void doRefreshShadeBatteryLevel() {
509 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
510 refreshBatteryLevelFuture = null;
513 private void doRefreshShade(RefreshKind kind) {
515 HDPowerViewHubHandler bridge;
516 if ((bridge = getBridgeHandler()) == null) {
517 throw new HubProcessingException("Missing bridge handler");
519 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
520 if (webTargets == null) {
521 throw new HubProcessingException("Web targets not initialized");
526 shadeData = webTargets.refreshShadePosition(shadeId);
527 updateShadePositions(shadeData);
528 updateDetectedCapabilities(shadeData);
531 Survey survey = webTargets.getShadeSurvey(shadeId);
532 if (survey.surveyData != null) {
533 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
535 logger.warn("No response from shade {} survey", shadeId);
539 shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
540 updateBatteryStates(shadeData.batteryStatus, shadeData.batteryStrength);
543 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
545 } catch (HubInvalidResponseException e) {
546 Throwable cause = e.getCause();
548 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
550 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
552 } catch (HubMaintenanceException e) {
553 // exceptions are logged in HDPowerViewWebTargets
554 } catch (HubShadeTimeoutException e) {
555 logger.info("Shade {} wireless refresh time out", shadeId);
556 } catch (HubException e) {
557 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
558 // for any ongoing requests. Logging this would only cause confusion.
560 logger.warn("Unexpected error: {}", e.getMessage());