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.core.library.types.DecimalType;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.PercentType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StopMoveType;
45 import org.openhab.core.library.types.UpDownType;
46 import org.openhab.core.library.unit.Units;
47 import org.openhab.core.thing.Bridge;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.UnDefType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
59 * Handles commands for an HD PowerView Shade
61 * @author Andy Lintner - Initial contribution
62 * @author Andrew Fiddian-Green - Added support for secondary rail positions
65 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
67 private enum RefreshKind {
73 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
74 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
76 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
77 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
78 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
79 private @Nullable Capabilities capabilities;
81 private boolean isDisposing;
83 public HDPowerViewShadeHandler(Thing thing) {
88 public void initialize() {
89 logger.debug("Initializing shade handler");
92 shadeId = getShadeId();
93 } catch (NumberFormatException e) {
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95 "@text/offline.conf-error.invalid-id");
98 Bridge bridge = getBridge();
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
103 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
105 "@text/offline.conf-error.invalid-bridge-handler");
108 ThingStatus bridgeStatus = bridge.getStatus();
109 if (bridgeStatus == ThingStatus.ONLINE) {
110 updateStatus(ThingStatus.UNKNOWN);
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
117 public void dispose() {
118 logger.debug("Disposing shade handler for shade {}", shadeId);
120 ScheduledFuture<?> future = refreshPositionFuture;
121 if (future != null) {
124 refreshPositionFuture = null;
125 future = refreshSignalFuture;
126 if (future != null) {
129 refreshSignalFuture = null;
130 future = refreshBatteryLevelFuture;
131 if (future != null) {
134 refreshBatteryLevelFuture = null;
139 public void handleCommand(ChannelUID channelUID, Command command) {
140 String channelId = channelUID.getId();
142 if (RefreshType.REFRESH == command) {
144 case CHANNEL_SHADE_POSITION:
145 case CHANNEL_SHADE_SECONDARY_POSITION:
146 case CHANNEL_SHADE_VANE:
147 requestRefreshShadePosition();
149 case CHANNEL_SHADE_LOW_BATTERY:
150 case CHANNEL_SHADE_BATTERY_LEVEL:
151 case CHANNEL_SHADE_BATTERY_VOLTAGE:
152 requestRefreshShadeBatteryLevel();
154 case CHANNEL_SHADE_SIGNAL_STRENGTH:
155 requestRefreshShadeSurvey();
161 HDPowerViewHubHandler bridge = getBridgeHandler();
162 if (bridge == null) {
163 logger.warn("Missing bridge handler");
166 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
167 if (webTargets == null) {
168 logger.warn("Web targets not initialized");
172 handleShadeCommand(channelId, command, webTargets, shadeId);
173 } catch (HubInvalidResponseException e) {
174 Throwable cause = e.getCause();
176 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
178 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
180 } catch (HubMaintenanceException e) {
181 // exceptions are logged in HDPowerViewWebTargets
182 } catch (HubException e) {
183 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
184 // for any ongoing requests. Logging this would only cause confusion.
186 logger.warn("Unexpected error: {}", e.getMessage());
191 private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
192 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
194 case CHANNEL_SHADE_POSITION:
195 if (command instanceof PercentType) {
196 moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue(), webTargets, shadeId);
197 } else if (command instanceof UpDownType) {
198 moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
199 } else if (command instanceof StopMoveType) {
200 if (StopMoveType.STOP == command) {
201 stopShade(webTargets, shadeId);
203 logger.warn("Unexpected StopMoveType command");
208 case CHANNEL_SHADE_VANE:
209 if (command instanceof PercentType) {
210 moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue(), webTargets, shadeId);
211 } else if (command instanceof OnOffType) {
212 moveShade(VANE_TILT_COORDS, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
216 case CHANNEL_SHADE_SECONDARY_POSITION:
217 if (command instanceof PercentType) {
218 moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue(), webTargets, shadeId);
219 } else if (command instanceof UpDownType) {
220 moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
221 } else if (command instanceof StopMoveType) {
222 if (StopMoveType.STOP == command) {
223 stopShade(webTargets, shadeId);
225 logger.warn("Unexpected StopMoveType command");
230 case CHANNEL_SHADE_CALIBRATE:
231 if (OnOffType.ON == command) {
232 calibrateShade(webTargets, shadeId);
239 * Update the state of the channels based on the ShadeData provided.
241 * @param shadeData the ShadeData to be used; may be null.
243 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
244 if (shadeData != null) {
245 updateStatus(ThingStatus.ONLINE);
246 updateCapabilities(shadeData);
247 updateSoftProperties(shadeData);
248 updateFirmwareProperties(shadeData);
249 ShadePosition shadePosition = shadeData.positions;
250 if (shadePosition != null) {
251 updatePositionStates(shadePosition);
253 updateBatteryLevelStates(shadeData.batteryStatus);
254 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
255 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
257 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
259 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
263 private void updateCapabilities(ShadeData shade) {
264 if (capabilities != null) {
268 Integer value = shade.capabilities;
270 int valueAsInt = value.intValue();
271 logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
272 capabilities = db.getCapabilities(valueAsInt);
274 logger.debug("Capabilities not included in shade response");
278 private Capabilities getCapabilitiesOrDefault() {
279 Capabilities capabilities = this.capabilities;
280 if (capabilities == null) {
281 return new Capabilities();
287 * Update the Thing's properties based on the contents of the provided ShadeData.
289 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
290 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
294 private void updateSoftProperties(ShadeData shadeData) {
295 final Map<String, String> properties = getThing().getProperties();
296 boolean propChanged = false;
298 // update 'type' property
299 final int type = shadeData.type;
300 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
301 String propOldVal = properties.getOrDefault(propKey, "");
302 String propNewVal = db.getType(type).toString();
303 if (!propNewVal.equals(propOldVal)) {
305 getThing().setProperty(propKey, propNewVal);
306 if ((type > 0) && !db.isTypeInDatabase(type)) {
307 db.logTypeNotInDatabase(type);
311 // update 'capabilities' property
312 final Integer temp = shadeData.capabilities;
313 final int capabilitiesVal = temp != null ? temp.intValue() : -1;
314 Capabilities capabilities = db.getCapabilities(capabilitiesVal);
315 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
316 propOldVal = properties.getOrDefault(propKey, "");
317 propNewVal = capabilities.toString();
318 if (!propNewVal.equals(propOldVal)) {
320 getThing().setProperty(propKey, propNewVal);
321 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
322 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
326 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
327 && (capabilitiesVal != db.getType(type).getCapabilities())) {
328 db.logCapabilitiesMismatch(type, capabilitiesVal);
332 private void updateFirmwareProperties(ShadeData shadeData) {
333 Map<String, String> properties = editProperties();
334 Firmware shadeFirmware = shadeData.firmware;
335 Firmware motorFirmware = shadeData.motor;
336 if (shadeFirmware != null) {
337 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
339 if (motorFirmware != null) {
340 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
342 updateProperties(properties);
346 * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
348 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
349 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
350 * potential need to add support for that type resp. capabilities.
354 private void updateHardProperties(ShadeData shadeData) {
355 final ShadePosition positions = shadeData.positions;
356 if (positions == null) {
359 Capabilities capabilities = getCapabilitiesOrDefault();
360 final Map<String, String> properties = getThing().getProperties();
362 // update 'secondary rail detected' property
363 String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
364 String propOldVal = properties.getOrDefault(propKey, "");
365 boolean propNewBool = positions.secondaryRailDetected();
366 String propNewVal = String.valueOf(propNewBool);
367 if (!propNewVal.equals(propOldVal)) {
368 getThing().setProperty(propKey, propNewVal);
369 if (propNewBool != capabilities.supportsSecondary()) {
370 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
374 // update 'tilt anywhere detected' property
375 propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
376 propOldVal = properties.getOrDefault(propKey, "");
377 propNewBool = positions.tiltAnywhereDetected();
378 propNewVal = String.valueOf(propNewBool);
379 if (!propNewVal.equals(propOldVal)) {
380 getThing().setProperty(propKey, propNewVal);
381 if (propNewBool != capabilities.supportsTiltAnywhere()) {
382 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
387 private void updatePositionStates(ShadePosition shadePos) {
388 Capabilities capabilities = this.capabilities;
389 if (capabilities == null) {
390 logger.debug("The 'shadeCapabilities' field has not yet been initialized");
391 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
392 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
393 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
396 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED));
397 updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_COORDS));
398 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN));
401 private void updateBatteryLevelStates(int batteryStatus) {
403 switch (batteryStatus) {
411 case 4: // Plugged in
414 default: // No status available (0) or invalid
415 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
416 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
419 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
420 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
423 private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
424 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
425 ShadePosition newPosition = null;
426 // (try to) read the positions from the hub
427 ShadeData shadeData = webTargets.getShade(shadeId);
428 updateCapabilities(shadeData);
429 newPosition = shadeData.positions;
430 // if no positions returned, then create a new position
431 if (newPosition == null) {
432 newPosition = new ShadePosition();
434 Capabilities capabilities = getCapabilitiesOrDefault();
435 // set the new position value, and write the positions to the hub
436 shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
437 updateShadePositions(shadeData);
440 private void stopShade(HDPowerViewWebTargets webTargets, int shadeId)
441 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
442 updateShadePositions(webTargets.stopShade(shadeId));
443 // Positions in response from stop motion is not updated to to actual positions yet,
444 // so we need to request hard refresh.
445 requestRefreshShadePosition();
448 private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId)
449 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
450 updateShadePositions(webTargets.calibrateShade(shadeId));
453 private void updateShadePositions(ShadeData shadeData) {
454 ShadePosition shadePosition = shadeData.positions;
455 if (shadePosition == null) {
458 updateCapabilities(shadeData);
459 updatePositionStates(shadePosition);
462 private int getShadeId() throws NumberFormatException {
463 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
465 throw new NumberFormatException("null input string");
467 return Integer.parseInt(str);
471 * Request that the shade shall undergo a 'hard' refresh for querying its current position
473 protected synchronized void requestRefreshShadePosition() {
474 if (refreshPositionFuture == null) {
475 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
480 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
482 protected synchronized void requestRefreshShadeSurvey() {
483 if (refreshSignalFuture == null) {
484 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
489 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
491 protected synchronized void requestRefreshShadeBatteryLevel() {
492 if (refreshBatteryLevelFuture == null) {
493 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
497 private void doRefreshShadePosition() {
498 this.doRefreshShade(RefreshKind.POSITION);
499 refreshPositionFuture = null;
502 private void doRefreshShadeSignal() {
503 this.doRefreshShade(RefreshKind.SURVEY);
504 refreshSignalFuture = null;
507 private void doRefreshShadeBatteryLevel() {
508 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
509 refreshBatteryLevelFuture = null;
512 private void doRefreshShade(RefreshKind kind) {
514 HDPowerViewHubHandler bridge;
515 if ((bridge = getBridgeHandler()) == null) {
516 throw new HubProcessingException("Missing bridge handler");
518 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
519 if (webTargets == null) {
520 throw new HubProcessingException("Web targets not initialized");
525 shadeData = webTargets.refreshShadePosition(shadeId);
528 Survey survey = webTargets.getShadeSurvey(shadeId);
529 if (survey.surveyData != null) {
530 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
532 logger.warn("No response from shade {} survey", shadeId);
536 shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
539 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
541 if (Boolean.TRUE.equals(shadeData.timedOut)) {
542 logger.warn("Shade {} wireless refresh time out", shadeId);
543 } else if (kind == RefreshKind.POSITION) {
544 updateShadePositions(shadeData);
545 updateHardProperties(shadeData);
547 } catch (HubInvalidResponseException e) {
548 Throwable cause = e.getCause();
550 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
552 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
554 } catch (HubMaintenanceException e) {
555 // exceptions are logged in HDPowerViewWebTargets
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());