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;
58 * Handles commands for an HD PowerView Shade
60 * @author Andy Lintner - Initial contribution
61 * @author Andrew Fiddian-Green - Added support for secondary rail positions
64 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
66 private enum RefreshKind {
72 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
74 private static final int REFRESH_DELAY_SEC = 10;
75 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
76 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
77 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
79 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
80 private int shadeCapabilities = -1;
82 public HDPowerViewShadeHandler(Thing thing) {
87 public void initialize() {
90 } catch (NumberFormatException e) {
91 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
92 "@text/offline.conf-error.invalid-id");
95 Bridge bridge = getBridge();
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
100 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
102 "@text/offline.conf-error.invalid-bridge-handler");
105 ThingStatus bridgeStatus = bridge.getStatus();
106 if (bridgeStatus == ThingStatus.ONLINE) {
107 updateStatus(ThingStatus.ONLINE);
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
114 public void handleCommand(ChannelUID channelUID, Command command) {
115 String channelId = channelUID.getId();
117 if (RefreshType.REFRESH == command) {
119 case CHANNEL_SHADE_POSITION:
120 case CHANNEL_SHADE_SECONDARY_POSITION:
121 case CHANNEL_SHADE_VANE:
122 requestRefreshShadePosition();
124 case CHANNEL_SHADE_LOW_BATTERY:
125 case CHANNEL_SHADE_BATTERY_LEVEL:
126 case CHANNEL_SHADE_BATTERY_VOLTAGE:
127 requestRefreshShadeBatteryLevel();
129 case CHANNEL_SHADE_SIGNAL_STRENGTH:
130 requestRefreshShadeSurvey();
137 case CHANNEL_SHADE_POSITION:
138 if (command instanceof PercentType) {
139 moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue());
140 } else if (command instanceof UpDownType) {
141 moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
142 } else if (command instanceof StopMoveType) {
143 if (StopMoveType.STOP.equals(command)) {
146 logger.warn("Unexpected StopMoveType command");
151 case CHANNEL_SHADE_VANE:
152 if (command instanceof PercentType) {
153 moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue());
154 } else if (command instanceof OnOffType) {
155 moveShade(VANE_TILT_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
159 case CHANNEL_SHADE_SECONDARY_POSITION:
160 if (command instanceof PercentType) {
161 moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue());
162 } else if (command instanceof UpDownType) {
163 moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
164 } else if (command instanceof StopMoveType) {
165 if (StopMoveType.STOP.equals(command)) {
168 logger.warn("Unexpected StopMoveType command");
176 * Update the state of the channels based on the ShadeData provided.
178 * @param shadeData the ShadeData to be used; may be null.
180 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
181 if (shadeData != null) {
182 updateStatus(ThingStatus.ONLINE);
183 updateSoftProperties(shadeData);
184 updateFirmwareProperties(shadeData);
185 updateBindingStates(shadeData.positions);
186 updateBatteryLevel(shadeData.batteryStatus);
187 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
188 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
190 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
197 * Update the Thing's properties based on the contents of the provided ShadeData.
199 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
200 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
204 private void updateSoftProperties(ShadeData shadeData) {
205 final Map<String, String> properties = getThing().getProperties();
206 boolean propChanged = false;
208 // update 'type' property
209 final int type = shadeData.type;
210 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
211 String propOldVal = properties.getOrDefault(propKey, "");
212 String propNewVal = db.getType(type).toString();
213 if (!propNewVal.equals(propOldVal)) {
215 getThing().setProperty(propKey, propNewVal);
216 if ((type > 0) && !db.isTypeInDatabase(type)) {
217 db.logTypeNotInDatabase(type);
221 // update 'capabilities' property
222 final Integer temp = shadeData.capabilities;
223 final int capabilitiesVal = temp != null ? temp.intValue() : -1;
224 Capabilities capabilities = db.getCapabilities(capabilitiesVal);
225 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
226 propOldVal = properties.getOrDefault(propKey, "");
227 propNewVal = capabilities.toString();
228 if (!propNewVal.equals(propOldVal)) {
230 getThing().setProperty(propKey, propNewVal);
231 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
232 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
236 // update shadeCapabilities field
237 if (capabilitiesVal >= 0) {
238 shadeCapabilities = capabilitiesVal;
241 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
242 && (capabilitiesVal != db.getType(type).getCapabilities())) {
243 db.logCapabilitiesMismatch(type, capabilitiesVal);
247 private void updateFirmwareProperties(ShadeData shadeData) {
248 Map<String, String> properties = editProperties();
249 Firmware shadeFirmware = shadeData.firmware;
250 Firmware motorFirmware = shadeData.motor;
251 if (shadeFirmware != null) {
252 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, shadeFirmware.toString());
254 if (motorFirmware != null) {
255 properties.put(PROPERTY_MOTOR_FIRMWARE_VERSION, motorFirmware.toString());
257 updateProperties(properties);
261 * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
263 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
264 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
265 * potential need to add support for that type resp. capabilities.
269 private void updateHardProperties(ShadeData shadeData) {
270 final ShadePosition positions = shadeData.positions;
271 if (positions != null) {
272 final Map<String, String> properties = getThing().getProperties();
274 // update 'jsonHasSecondary' property
275 String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
276 String propOldVal = properties.getOrDefault(propKey, "");
277 boolean propNewBool = positions.secondaryRailDetected();
278 String propNewVal = String.valueOf(propNewBool);
279 if (!propNewVal.equals(propOldVal)) {
280 getThing().setProperty(propKey, propNewVal);
281 final Integer temp = shadeData.capabilities;
282 final int capabilities = temp != null ? temp.intValue() : -1;
283 if (propNewBool != db.getCapabilities(capabilities).supportsSecondary()) {
284 db.logPropertyMismatch(propKey, shadeData.type, capabilities, propNewBool);
288 // update 'jsonTiltAnywhere' property
289 propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
290 propOldVal = properties.getOrDefault(propKey, "");
291 propNewBool = positions.tiltAnywhereDetected();
292 propNewVal = String.valueOf(propNewBool);
293 if (!propNewVal.equals(propOldVal)) {
294 getThing().setProperty(propKey, propNewVal);
295 final Integer temp = shadeData.capabilities;
296 final int capabilities = temp != null ? temp.intValue() : -1;
297 if (propNewBool != db.getCapabilities(capabilities).supportsTiltAnywhere()) {
298 db.logPropertyMismatch(propKey, shadeData.type, capabilities, propNewBool);
304 private void updateBindingStates(@Nullable ShadePosition shadePos) {
305 if (shadePos == null) {
306 logger.debug("The value of 'shadePosition' argument was null!");
307 } else if (shadeCapabilities < 0) {
308 logger.debug("The 'shadeCapabilities' field has not been initialized!");
310 Capabilities caps = db.getCapabilities(shadeCapabilities);
311 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(caps, PRIMARY_ZERO_IS_CLOSED));
312 updateState(CHANNEL_SHADE_VANE, shadePos.getState(caps, VANE_TILT_COORDS));
313 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(caps, SECONDARY_ZERO_IS_OPEN));
316 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
317 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
318 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
321 private void updateBatteryLevel(int batteryStatus) {
323 switch (batteryStatus) {
331 case 4: // Plugged in
334 default: // No status available (0) or invalid
335 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
336 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
339 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
340 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
343 private void moveShade(CoordinateSystem coordSys, int newPercent) {
345 HDPowerViewHubHandler bridge;
346 if ((bridge = getBridgeHandler()) == null) {
347 throw new HubProcessingException("Missing bridge handler");
349 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
350 if (webTargets == null) {
351 throw new HubProcessingException("Web targets not initialized");
353 ShadePosition newPosition = null;
354 // (try to) read the positions from the hub
355 int shadeId = getShadeId();
356 Shade shade = webTargets.getShade(shadeId);
358 ShadeData shadeData = shade.shade;
359 if (shadeData != null) {
360 newPosition = shadeData.positions;
363 // if no positions returned, then create a new position
364 if (newPosition == null) {
365 newPosition = new ShadePosition();
367 // set the new position value, and write the positions to the hub
368 webTargets.moveShade(shadeId,
369 newPosition.setPosition(db.getCapabilities(shadeCapabilities), coordSys, newPercent));
370 // update the Channels to match the new position
371 final ShadePosition finalPosition = newPosition;
372 scheduler.submit(() -> {
373 updateBindingStates(finalPosition);
375 } catch (HubProcessingException | NumberFormatException e) {
376 logger.warn("Unexpected error: {}", e.getMessage());
378 } catch (HubMaintenanceException e) {
379 // exceptions are logged in HDPowerViewWebTargets
384 private int getShadeId() throws NumberFormatException {
385 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
387 throw new NumberFormatException("null input string");
389 return Integer.parseInt(str);
392 private void stopShade() {
394 HDPowerViewHubHandler bridge;
395 if ((bridge = getBridgeHandler()) == null) {
396 throw new HubProcessingException("Missing bridge handler");
398 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
399 if (webTargets == null) {
400 throw new HubProcessingException("Web targets not initialized");
402 int shadeId = getShadeId();
403 webTargets.stopShade(shadeId);
404 requestRefreshShadePosition();
405 } catch (HubProcessingException | NumberFormatException e) {
406 logger.warn("Unexpected error: {}", e.getMessage());
408 } catch (HubMaintenanceException e) {
409 // exceptions are logged in HDPowerViewWebTargets
415 * Request that the shade shall undergo a 'hard' refresh for querying its current position
417 protected synchronized void requestRefreshShadePosition() {
418 if (refreshPositionFuture == null) {
419 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
425 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
427 protected synchronized void requestRefreshShadeSurvey() {
428 if (refreshSignalFuture == null) {
429 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, REFRESH_DELAY_SEC, TimeUnit.SECONDS);
434 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
436 protected synchronized void requestRefreshShadeBatteryLevel() {
437 if (refreshBatteryLevelFuture == null) {
438 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
443 private void doRefreshShadePosition() {
444 this.doRefreshShade(RefreshKind.POSITION);
445 refreshPositionFuture = null;
448 private void doRefreshShadeSignal() {
449 this.doRefreshShade(RefreshKind.SURVEY);
450 refreshSignalFuture = null;
453 private void doRefreshShadeBatteryLevel() {
454 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
455 refreshBatteryLevelFuture = null;
458 private void doRefreshShade(RefreshKind kind) {
460 HDPowerViewHubHandler bridge;
461 if ((bridge = getBridgeHandler()) == null) {
462 throw new HubProcessingException("Missing bridge handler");
464 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
465 if (webTargets == null) {
466 throw new HubProcessingException("Web targets not initialized");
468 int shadeId = getShadeId();
472 shade = webTargets.refreshShadePosition(shadeId);
475 Survey survey = webTargets.getShadeSurvey(shadeId);
476 if (survey != null && survey.surveyData != null) {
477 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
479 logger.warn("No response from shade {} survey", shadeId);
483 shade = webTargets.refreshShadeBatteryLevel(shadeId);
486 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
489 ShadeData shadeData = shade.shade;
490 if (shadeData != null) {
491 if (Boolean.TRUE.equals(shadeData.timedOut)) {
492 logger.warn("Shade {} wireless refresh time out", shadeId);
493 } else if (kind == RefreshKind.POSITION) {
494 updateHardProperties(shadeData);
498 } catch (HubProcessingException | NumberFormatException e) {
499 logger.warn("Unexpected error: {}", e.getMessage());
500 } catch (HubMaintenanceException e) {
501 // exceptions are logged in HDPowerViewWebTargets