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.StringType;
46 import org.openhab.core.library.types.UpDownType;
47 import org.openhab.core.library.unit.Units;
48 import org.openhab.core.thing.Bridge;
49 import org.openhab.core.thing.ChannelUID;
50 import org.openhab.core.thing.Thing;
51 import org.openhab.core.thing.ThingStatus;
52 import org.openhab.core.thing.ThingStatusDetail;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.openhab.core.types.UnDefType;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * Handles commands for an HD PowerView Shade
62 * @author Andy Lintner - Initial contribution
63 * @author Andrew Fiddian-Green - Added support for secondary rail positions
66 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
68 private enum RefreshKind {
74 private static final String COMMAND_CALIBRATE = "CALIBRATE";
76 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
77 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
79 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
80 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
81 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
82 private @Nullable Capabilities capabilities;
84 private boolean isDisposing;
86 public HDPowerViewShadeHandler(Thing thing) {
91 public void initialize() {
93 shadeId = getConfigAs(HDPowerViewShadeConfiguration.class).id;
94 logger.debug("Initializing shade handler for shade {}", shadeId);
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
97 "@text/offline.conf-error.invalid-id");
100 Bridge bridge = getBridge();
101 if (bridge == null) {
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
105 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
106 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
107 "@text/offline.conf-error.invalid-bridge-handler");
110 updateStatus(ThingStatus.UNKNOWN);
114 public void dispose() {
115 logger.debug("Disposing shade handler for shade {}", shadeId);
117 ScheduledFuture<?> future = refreshPositionFuture;
118 if (future != null) {
121 refreshPositionFuture = null;
122 future = refreshSignalFuture;
123 if (future != null) {
126 refreshSignalFuture = null;
127 future = refreshBatteryLevelFuture;
128 if (future != null) {
131 refreshBatteryLevelFuture = null;
136 public void handleCommand(ChannelUID channelUID, Command command) {
137 String channelId = channelUID.getId();
139 if (RefreshType.REFRESH == command) {
141 case CHANNEL_SHADE_POSITION:
142 case CHANNEL_SHADE_SECONDARY_POSITION:
143 case CHANNEL_SHADE_VANE:
144 requestRefreshShadePosition();
146 case CHANNEL_SHADE_LOW_BATTERY:
147 case CHANNEL_SHADE_BATTERY_LEVEL:
148 case CHANNEL_SHADE_BATTERY_VOLTAGE:
149 requestRefreshShadeBatteryLevel();
151 case CHANNEL_SHADE_SIGNAL_STRENGTH:
152 requestRefreshShadeSurvey();
158 HDPowerViewHubHandler bridge = getBridgeHandler();
159 if (bridge == null) {
160 logger.warn("Missing bridge handler");
163 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
164 if (webTargets == null) {
165 logger.warn("Web targets not initialized");
169 handleShadeCommand(channelId, command, webTargets, shadeId);
170 } catch (HubInvalidResponseException e) {
171 Throwable cause = e.getCause();
173 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
175 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
177 } catch (HubMaintenanceException e) {
178 // exceptions are logged in HDPowerViewWebTargets
179 } catch (HubException e) {
180 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
181 // for any ongoing requests. Logging this would only cause confusion.
183 logger.warn("Unexpected error: {}", e.getMessage());
188 private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
189 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
191 case CHANNEL_SHADE_POSITION:
192 if (command instanceof PercentType) {
193 moveShade(PRIMARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
194 } else if (command instanceof UpDownType) {
195 moveShade(PRIMARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
196 } else if (command instanceof StopMoveType) {
197 if (StopMoveType.STOP == command) {
198 stopShade(webTargets, shadeId);
200 logger.warn("Unexpected StopMoveType command");
205 case CHANNEL_SHADE_VANE:
206 if (command instanceof PercentType) {
207 moveShade(VANE_TILT_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
208 } else if (command instanceof OnOffType) {
209 moveShade(VANE_TILT_POSITION, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
213 case CHANNEL_SHADE_SECONDARY_POSITION:
214 if (command instanceof PercentType) {
215 moveShade(SECONDARY_POSITION, ((PercentType) command).intValue(), webTargets, shadeId);
216 } else if (command instanceof UpDownType) {
217 moveShade(SECONDARY_POSITION, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
218 } else if (command instanceof StopMoveType) {
219 if (StopMoveType.STOP == command) {
220 stopShade(webTargets, shadeId);
222 logger.warn("Unexpected StopMoveType command");
227 case CHANNEL_SHADE_COMMAND:
228 if (command instanceof StringType) {
229 if (COMMAND_CALIBRATE.equals(((StringType) command).toString())) {
230 logger.debug("Calibrate shade {}", shadeId);
231 calibrateShade(webTargets, shadeId);
234 logger.warn("Unsupported command: {}. Supported commands are: " + COMMAND_CALIBRATE, command);
241 * Update the state of the channels based on the ShadeData provided.
243 * @param shadeData the ShadeData to be used; may be null.
245 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
246 if (shadeData != null) {
247 updateStatus(ThingStatus.ONLINE);
248 updateCapabilities(shadeData);
249 updateSoftProperties(shadeData);
250 updateFirmwareProperties(shadeData);
251 ShadePosition shadePosition = shadeData.positions;
252 if (shadePosition != null) {
253 updatePositionStates(shadePosition);
255 updateBatteryLevelStates(shadeData.batteryStatus);
256 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
257 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
259 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
261 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
265 private void updateCapabilities(ShadeData shade) {
266 if (capabilities != null) {
270 Integer value = shade.capabilities;
272 int valueAsInt = value.intValue();
273 logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
274 capabilities = db.getCapabilities(valueAsInt);
276 logger.debug("Capabilities not included in shade response");
280 private Capabilities getCapabilitiesOrDefault() {
281 Capabilities capabilities = this.capabilities;
282 if (capabilities == null) {
283 return new Capabilities();
289 * Update the Thing's properties based on the contents of the provided ShadeData.
291 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
292 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
296 private void updateSoftProperties(ShadeData shadeData) {
297 final Map<String, String> properties = getThing().getProperties();
298 boolean propChanged = false;
300 // update 'type' property
301 final int type = shadeData.type;
302 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
303 String propOldVal = properties.getOrDefault(propKey, "");
304 String propNewVal = db.getType(type).toString();
305 if (!propNewVal.equals(propOldVal)) {
307 getThing().setProperty(propKey, propNewVal);
308 if ((type > 0) && !db.isTypeInDatabase(type)) {
309 db.logTypeNotInDatabase(type);
313 // update 'capabilities' property
314 final Integer temp = shadeData.capabilities;
315 final int capabilitiesVal = temp != null ? temp.intValue() : -1;
316 Capabilities capabilities = db.getCapabilities(capabilitiesVal);
317 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
318 propOldVal = properties.getOrDefault(propKey, "");
319 propNewVal = capabilities.toString();
320 if (!propNewVal.equals(propOldVal)) {
322 getThing().setProperty(propKey, propNewVal);
323 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
324 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
328 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
329 && (capabilitiesVal != db.getType(type).getCapabilities())) {
330 db.logCapabilitiesMismatch(type, capabilitiesVal);
334 private void updateFirmwareProperties(ShadeData shadeData) {
335 Map<String, String> properties = editProperties();
336 Firmware shadeFirmware = shadeData.firmware;
337 Firmware motorFirmware = shadeData.motor;
338 if (shadeFirmware != null) {
339 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
341 if (motorFirmware != null) {
342 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
344 updateProperties(properties);
348 * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
350 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
351 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
352 * potential need to add support for that type resp. capabilities.
356 private void updateHardProperties(ShadeData shadeData) {
357 final ShadePosition positions = shadeData.positions;
358 if (positions == null) {
361 Capabilities capabilities = getCapabilitiesOrDefault();
362 final Map<String, String> properties = getThing().getProperties();
364 // update 'secondary rail detected' property
365 String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
366 String propOldVal = properties.getOrDefault(propKey, "");
367 boolean propNewBool = positions.secondaryRailDetected();
368 String propNewVal = String.valueOf(propNewBool);
369 if (!propNewVal.equals(propOldVal)) {
370 getThing().setProperty(propKey, propNewVal);
371 if (propNewBool != capabilities.supportsSecondary()) {
372 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
376 // update 'tilt anywhere detected' property
377 propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
378 propOldVal = properties.getOrDefault(propKey, "");
379 propNewBool = positions.tiltAnywhereDetected();
380 propNewVal = String.valueOf(propNewBool);
381 if (!propNewVal.equals(propOldVal)) {
382 getThing().setProperty(propKey, propNewVal);
383 if (propNewBool != capabilities.supportsTiltAnywhere()) {
384 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
389 private void updatePositionStates(ShadePosition shadePos) {
390 Capabilities capabilities = this.capabilities;
391 if (capabilities == null) {
392 logger.debug("The 'shadeCapabilities' field has not yet been initialized");
393 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
394 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
395 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
398 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_POSITION));
399 updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_POSITION));
400 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_POSITION));
403 private void updateBatteryLevelStates(int batteryStatus) {
405 switch (batteryStatus) {
413 case 4: // Plugged in
416 default: // No status available (0) or invalid
417 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
418 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
421 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
422 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
425 private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
426 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
427 ShadePosition newPosition = null;
428 // (try to) read the positions from the hub
429 ShadeData shadeData = webTargets.getShade(shadeId);
430 updateCapabilities(shadeData);
431 newPosition = shadeData.positions;
432 // if no positions returned, then create a new position
433 if (newPosition == null) {
434 newPosition = new ShadePosition();
436 Capabilities capabilities = getCapabilitiesOrDefault();
437 // set the new position value, and write the positions to the hub
438 shadeData = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
439 updateShadePositions(shadeData);
442 private void stopShade(HDPowerViewWebTargets webTargets, int shadeId)
443 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
444 updateShadePositions(webTargets.stopShade(shadeId));
445 // Positions in response from stop motion is not updated to to actual positions yet,
446 // so we need to request hard refresh.
447 requestRefreshShadePosition();
450 private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId)
451 throws HubInvalidResponseException, HubProcessingException, HubMaintenanceException {
452 updateShadePositions(webTargets.calibrateShade(shadeId));
455 private void updateShadePositions(ShadeData shadeData) {
456 ShadePosition shadePosition = shadeData.positions;
457 if (shadePosition == null) {
460 updateCapabilities(shadeData);
461 updatePositionStates(shadePosition);
465 * Request that the shade shall undergo a 'hard' refresh for querying its current position
467 protected synchronized void requestRefreshShadePosition() {
468 if (refreshPositionFuture == null) {
469 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
474 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
476 protected synchronized void requestRefreshShadeSurvey() {
477 if (refreshSignalFuture == null) {
478 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
483 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
485 protected synchronized void requestRefreshShadeBatteryLevel() {
486 if (refreshBatteryLevelFuture == null) {
487 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
491 private void doRefreshShadePosition() {
492 this.doRefreshShade(RefreshKind.POSITION);
493 refreshPositionFuture = null;
496 private void doRefreshShadeSignal() {
497 this.doRefreshShade(RefreshKind.SURVEY);
498 refreshSignalFuture = null;
501 private void doRefreshShadeBatteryLevel() {
502 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
503 refreshBatteryLevelFuture = null;
506 private void doRefreshShade(RefreshKind kind) {
508 HDPowerViewHubHandler bridge;
509 if ((bridge = getBridgeHandler()) == null) {
510 throw new HubProcessingException("Missing bridge handler");
512 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
513 if (webTargets == null) {
514 throw new HubProcessingException("Web targets not initialized");
519 shadeData = webTargets.refreshShadePosition(shadeId);
522 Survey survey = webTargets.getShadeSurvey(shadeId);
523 if (survey.surveyData != null) {
524 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
526 logger.warn("No response from shade {} survey", shadeId);
530 shadeData = webTargets.refreshShadeBatteryLevel(shadeId);
533 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
535 if (Boolean.TRUE.equals(shadeData.timedOut)) {
536 logger.warn("Shade {} wireless refresh time out", shadeId);
537 } else if (kind == RefreshKind.POSITION) {
538 updateShadePositions(shadeData);
539 updateHardProperties(shadeData);
541 } catch (HubInvalidResponseException e) {
542 Throwable cause = e.getCause();
544 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
546 logger.warn("Bridge returned a bad JSON response: {} -> {}", e.getMessage(), cause.getMessage());
548 } catch (HubMaintenanceException e) {
549 // exceptions are logged in HDPowerViewWebTargets
550 } catch (HubException e) {
551 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
552 // for any ongoing requests. Logging this would only cause confusion.
554 logger.warn("Unexpected error: {}", e.getMessage());