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.HubMaintenanceException;
29 import org.openhab.binding.hdpowerview.internal.HubProcessingException;
30 import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
31 import org.openhab.binding.hdpowerview.internal.api.Firmware;
32 import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
35 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
36 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
37 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
38 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.types.StopMoveType;
44 import org.openhab.core.library.types.UpDownType;
45 import org.openhab.core.library.unit.Units;
46 import org.openhab.core.thing.Bridge;
47 import org.openhab.core.thing.ChannelUID;
48 import org.openhab.core.thing.Thing;
49 import org.openhab.core.thing.ThingStatus;
50 import org.openhab.core.thing.ThingStatusDetail;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
57 import com.google.gson.JsonParseException;
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 final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
75 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
77 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
78 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
79 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
80 private @Nullable Capabilities capabilities;
82 private boolean isDisposing;
84 public HDPowerViewShadeHandler(Thing thing) {
89 public void initialize() {
90 logger.debug("Initializing shade handler");
93 shadeId = getShadeId();
94 } catch (NumberFormatException e) {
95 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
96 "@text/offline.conf-error.invalid-id");
99 Bridge bridge = getBridge();
100 if (bridge == null) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
104 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
106 "@text/offline.conf-error.invalid-bridge-handler");
109 ThingStatus bridgeStatus = bridge.getStatus();
110 if (bridgeStatus == ThingStatus.ONLINE) {
111 updateStatus(ThingStatus.UNKNOWN);
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
118 public void dispose() {
119 logger.debug("Disposing shade handler for shade {}", shadeId);
121 ScheduledFuture<?> future = refreshPositionFuture;
122 if (future != null) {
125 refreshPositionFuture = null;
126 future = refreshSignalFuture;
127 if (future != null) {
130 refreshSignalFuture = null;
131 future = refreshBatteryLevelFuture;
132 if (future != null) {
135 refreshBatteryLevelFuture = null;
140 public void handleCommand(ChannelUID channelUID, Command command) {
141 String channelId = channelUID.getId();
143 if (RefreshType.REFRESH == command) {
145 case CHANNEL_SHADE_POSITION:
146 case CHANNEL_SHADE_SECONDARY_POSITION:
147 case CHANNEL_SHADE_VANE:
148 requestRefreshShadePosition();
150 case CHANNEL_SHADE_LOW_BATTERY:
151 case CHANNEL_SHADE_BATTERY_LEVEL:
152 case CHANNEL_SHADE_BATTERY_VOLTAGE:
153 requestRefreshShadeBatteryLevel();
155 case CHANNEL_SHADE_SIGNAL_STRENGTH:
156 requestRefreshShadeSurvey();
162 HDPowerViewHubHandler bridge = getBridgeHandler();
163 if (bridge == null) {
164 logger.warn("Missing bridge handler");
167 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
168 if (webTargets == null) {
169 logger.warn("Web targets not initialized");
173 handleShadeCommand(channelId, command, webTargets, shadeId);
174 } catch (JsonParseException e) {
175 logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
176 } catch (HubProcessingException e) {
177 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
178 // for any ongoing requests. Logging this would only cause confusion.
180 logger.warn("Unexpected error: {}", e.getMessage());
182 } catch (HubMaintenanceException e) {
183 // exceptions are logged in HDPowerViewWebTargets
187 private void handleShadeCommand(String channelId, Command command, HDPowerViewWebTargets webTargets, int shadeId)
188 throws HubProcessingException, HubMaintenanceException {
190 case CHANNEL_SHADE_POSITION:
191 if (command instanceof PercentType) {
192 moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue(), webTargets, shadeId);
193 } else if (command instanceof UpDownType) {
194 moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
195 } else if (command instanceof StopMoveType) {
196 if (StopMoveType.STOP == command) {
197 stopShade(webTargets, shadeId);
199 logger.warn("Unexpected StopMoveType command");
204 case CHANNEL_SHADE_VANE:
205 if (command instanceof PercentType) {
206 moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue(), webTargets, shadeId);
207 } else if (command instanceof OnOffType) {
208 moveShade(VANE_TILT_COORDS, OnOffType.ON == command ? 100 : 0, webTargets, shadeId);
212 case CHANNEL_SHADE_SECONDARY_POSITION:
213 if (command instanceof PercentType) {
214 moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue(), webTargets, shadeId);
215 } else if (command instanceof UpDownType) {
216 moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP == command ? 0 : 100, webTargets, shadeId);
217 } else if (command instanceof StopMoveType) {
218 if (StopMoveType.STOP == command) {
219 stopShade(webTargets, shadeId);
221 logger.warn("Unexpected StopMoveType command");
226 case CHANNEL_SHADE_CALIBRATE:
227 if (OnOffType.ON == command) {
228 calibrateShade(webTargets, shadeId);
235 * Update the state of the channels based on the ShadeData provided.
237 * @param shadeData the ShadeData to be used; may be null.
239 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
240 if (shadeData != null) {
241 updateStatus(ThingStatus.ONLINE);
242 updateCapabilities(shadeData);
243 updateSoftProperties(shadeData);
244 updateFirmwareProperties(shadeData);
245 ShadePosition shadePosition = shadeData.positions;
246 if (shadePosition != null) {
247 updatePositionStates(shadePosition);
249 updateBatteryLevelStates(shadeData.batteryStatus);
250 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
251 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
253 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
255 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
259 private void updateCapabilities(ShadeData shade) {
260 if (capabilities != null) {
264 Integer value = shade.capabilities;
266 int valueAsInt = value.intValue();
267 logger.debug("Caching capabilities {} for shade {}", valueAsInt, shade.id);
268 capabilities = db.getCapabilities(valueAsInt);
270 logger.debug("Capabilities not included in shade response");
274 private Capabilities getCapabilitiesOrDefault() {
275 Capabilities capabilities = this.capabilities;
276 if (capabilities == null) {
277 return new Capabilities();
283 * Update the Thing's properties based on the contents of the provided ShadeData.
285 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
286 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
290 private void updateSoftProperties(ShadeData shadeData) {
291 final Map<String, String> properties = getThing().getProperties();
292 boolean propChanged = false;
294 // update 'type' property
295 final int type = shadeData.type;
296 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
297 String propOldVal = properties.getOrDefault(propKey, "");
298 String propNewVal = db.getType(type).toString();
299 if (!propNewVal.equals(propOldVal)) {
301 getThing().setProperty(propKey, propNewVal);
302 if ((type > 0) && !db.isTypeInDatabase(type)) {
303 db.logTypeNotInDatabase(type);
307 // update 'capabilities' property
308 final Integer temp = shadeData.capabilities;
309 final int capabilitiesVal = temp != null ? temp.intValue() : -1;
310 Capabilities capabilities = db.getCapabilities(capabilitiesVal);
311 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
312 propOldVal = properties.getOrDefault(propKey, "");
313 propNewVal = capabilities.toString();
314 if (!propNewVal.equals(propOldVal)) {
316 getThing().setProperty(propKey, propNewVal);
317 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
318 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
322 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
323 && (capabilitiesVal != db.getType(type).getCapabilities())) {
324 db.logCapabilitiesMismatch(type, capabilitiesVal);
328 private void updateFirmwareProperties(ShadeData shadeData) {
329 Map<String, String> properties = editProperties();
330 Firmware shadeFirmware = shadeData.firmware;
331 Firmware motorFirmware = shadeData.motor;
332 if (shadeFirmware != null) {
333 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
335 if (motorFirmware != null) {
336 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
338 updateProperties(properties);
342 * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
344 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
345 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
346 * potential need to add support for that type resp. capabilities.
350 private void updateHardProperties(ShadeData shadeData) {
351 final ShadePosition positions = shadeData.positions;
352 if (positions == null) {
355 Capabilities capabilities = getCapabilitiesOrDefault();
356 final Map<String, String> properties = getThing().getProperties();
358 // update 'secondary rail detected' property
359 String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
360 String propOldVal = properties.getOrDefault(propKey, "");
361 boolean propNewBool = positions.secondaryRailDetected();
362 String propNewVal = String.valueOf(propNewBool);
363 if (!propNewVal.equals(propOldVal)) {
364 getThing().setProperty(propKey, propNewVal);
365 if (propNewBool != capabilities.supportsSecondary()) {
366 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
370 // update 'tilt anywhere detected' property
371 propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
372 propOldVal = properties.getOrDefault(propKey, "");
373 propNewBool = positions.tiltAnywhereDetected();
374 propNewVal = String.valueOf(propNewBool);
375 if (!propNewVal.equals(propOldVal)) {
376 getThing().setProperty(propKey, propNewVal);
377 if (propNewBool != capabilities.supportsTiltAnywhere()) {
378 db.logPropertyMismatch(propKey, shadeData.type, capabilities.getValue(), propNewBool);
383 private void updatePositionStates(ShadePosition shadePos) {
384 Capabilities capabilities = this.capabilities;
385 if (capabilities == null) {
386 logger.debug("The 'shadeCapabilities' field has not yet been initialized");
387 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
388 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
389 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
392 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(capabilities, PRIMARY_ZERO_IS_CLOSED));
393 updateState(CHANNEL_SHADE_VANE, shadePos.getState(capabilities, VANE_TILT_COORDS));
394 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(capabilities, SECONDARY_ZERO_IS_OPEN));
397 private void updateBatteryLevelStates(int batteryStatus) {
399 switch (batteryStatus) {
407 case 4: // Plugged in
410 default: // No status available (0) or invalid
411 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
412 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
415 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
416 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
419 private void moveShade(CoordinateSystem coordSys, int newPercent, HDPowerViewWebTargets webTargets, int shadeId)
420 throws HubProcessingException, HubMaintenanceException {
421 ShadePosition newPosition = null;
422 // (try to) read the positions from the hub
423 Shade shade = webTargets.getShade(shadeId);
425 ShadeData shadeData = shade.shade;
426 if (shadeData != null) {
427 updateCapabilities(shadeData);
428 newPosition = shadeData.positions;
431 // if no positions returned, then create a new position
432 if (newPosition == null) {
433 newPosition = new ShadePosition();
435 Capabilities capabilities = getCapabilitiesOrDefault();
436 // set the new position value, and write the positions to the hub
437 shade = webTargets.moveShade(shadeId, newPosition.setPosition(capabilities, coordSys, newPercent));
439 updateShadePositions(shade);
443 private void stopShade(HDPowerViewWebTargets webTargets, int shadeId)
444 throws HubProcessingException, HubMaintenanceException {
445 Shade shade = webTargets.stopShade(shadeId);
447 updateShadePositions(shade);
449 // Positions in response from stop motion is not updated to to actual positions yet,
450 // so we need to request hard refresh.
451 requestRefreshShadePosition();
454 private void calibrateShade(HDPowerViewWebTargets webTargets, int shadeId)
455 throws HubProcessingException, HubMaintenanceException {
456 Shade shade = webTargets.calibrateShade(shadeId);
458 updateShadePositions(shade);
462 private void updateShadePositions(Shade shade) {
463 ShadeData shadeData = shade.shade;
464 if (shadeData == null) {
467 ShadePosition shadePosition = shadeData.positions;
468 if (shadePosition == null) {
471 updateCapabilities(shadeData);
472 updatePositionStates(shadePosition);
475 private int getShadeId() throws NumberFormatException {
476 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
478 throw new NumberFormatException("null input string");
480 return Integer.parseInt(str);
484 * Request that the shade shall undergo a 'hard' refresh for querying its current position
486 protected synchronized void requestRefreshShadePosition() {
487 if (refreshPositionFuture == null) {
488 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, 0, TimeUnit.SECONDS);
493 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
495 protected synchronized void requestRefreshShadeSurvey() {
496 if (refreshSignalFuture == null) {
497 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, 0, TimeUnit.SECONDS);
502 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
504 protected synchronized void requestRefreshShadeBatteryLevel() {
505 if (refreshBatteryLevelFuture == null) {
506 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, 0, TimeUnit.SECONDS);
510 private void doRefreshShadePosition() {
511 this.doRefreshShade(RefreshKind.POSITION);
512 refreshPositionFuture = null;
515 private void doRefreshShadeSignal() {
516 this.doRefreshShade(RefreshKind.SURVEY);
517 refreshSignalFuture = null;
520 private void doRefreshShadeBatteryLevel() {
521 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
522 refreshBatteryLevelFuture = null;
525 private void doRefreshShade(RefreshKind kind) {
527 HDPowerViewHubHandler bridge;
528 if ((bridge = getBridgeHandler()) == null) {
529 throw new HubProcessingException("Missing bridge handler");
531 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
532 if (webTargets == null) {
533 throw new HubProcessingException("Web targets not initialized");
538 shade = webTargets.refreshShadePosition(shadeId);
541 Survey survey = webTargets.getShadeSurvey(shadeId);
542 if (survey != null && survey.surveyData != null) {
543 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
545 logger.warn("No response from shade {} survey", shadeId);
549 shade = webTargets.refreshShadeBatteryLevel(shadeId);
552 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
555 ShadeData shadeData = shade.shade;
556 if (shadeData != null) {
557 if (Boolean.TRUE.equals(shadeData.timedOut)) {
558 logger.warn("Shade {} wireless refresh time out", shadeId);
559 } else if (kind == RefreshKind.POSITION) {
560 updateShadePositions(shade);
561 updateHardProperties(shadeData);
565 } catch (HubProcessingException e) {
566 // ScheduledFutures will be cancelled by dispose(), naturally causing InterruptedException in invoke()
567 // for any ongoing requests. Logging this would only cause confusion.
569 logger.warn("Unexpected error: {}", e.getMessage());
571 } catch (HubMaintenanceException e) {
572 // exceptions are logged in HDPowerViewWebTargets