2 * Copyright (c) 2010-2021 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.ShadePosition;
32 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
33 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
34 import org.openhab.binding.hdpowerview.internal.api.responses.Survey;
35 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
36 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase;
37 import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.PercentType;
41 import org.openhab.core.library.types.QuantityType;
42 import org.openhab.core.library.types.StopMoveType;
43 import org.openhab.core.library.types.UpDownType;
44 import org.openhab.core.library.unit.Units;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * Handles commands for an HD PowerView Shade
59 * @author Andy Lintner - Initial contribution
60 * @author Andrew Fiddian-Green - Added support for secondary rail positions
63 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
65 private enum RefreshKind {
71 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
73 private static final int REFRESH_DELAY_SEC = 10;
74 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
75 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
76 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
78 private final ShadeCapabilitiesDatabase db = new ShadeCapabilitiesDatabase();
79 private int shadeCapabilities = -1;
81 public HDPowerViewShadeHandler(Thing thing) {
86 public void initialize() {
89 } catch (NumberFormatException e) {
90 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
91 "@text/offline.conf-error.invalid-id");
94 Bridge bridge = getBridge();
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
99 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
101 "@text/offline.conf-error.invalid-bridge-handler");
104 ThingStatus bridgeStatus = bridge.getStatus();
105 if (bridgeStatus == ThingStatus.ONLINE) {
106 updateStatus(ThingStatus.ONLINE);
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
113 public void handleCommand(ChannelUID channelUID, Command command) {
114 String channelId = channelUID.getId();
116 if (RefreshType.REFRESH == command) {
118 case CHANNEL_SHADE_POSITION:
119 case CHANNEL_SHADE_SECONDARY_POSITION:
120 case CHANNEL_SHADE_VANE:
121 requestRefreshShadePosition();
123 case CHANNEL_SHADE_LOW_BATTERY:
124 case CHANNEL_SHADE_BATTERY_LEVEL:
125 case CHANNEL_SHADE_BATTERY_VOLTAGE:
126 requestRefreshShadeBatteryLevel();
128 case CHANNEL_SHADE_SIGNAL_STRENGTH:
129 requestRefreshShadeSurvey();
136 case CHANNEL_SHADE_POSITION:
137 if (command instanceof PercentType) {
138 moveShade(PRIMARY_ZERO_IS_CLOSED, ((PercentType) command).intValue());
139 } else if (command instanceof UpDownType) {
140 moveShade(PRIMARY_ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
141 } else if (command instanceof StopMoveType) {
142 if (StopMoveType.STOP.equals(command)) {
145 logger.warn("Unexpected StopMoveType command");
150 case CHANNEL_SHADE_VANE:
151 if (command instanceof PercentType) {
152 moveShade(VANE_TILT_COORDS, ((PercentType) command).intValue());
153 } else if (command instanceof OnOffType) {
154 moveShade(VANE_TILT_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
158 case CHANNEL_SHADE_SECONDARY_POSITION:
159 if (command instanceof PercentType) {
160 moveShade(SECONDARY_ZERO_IS_OPEN, ((PercentType) command).intValue());
161 } else if (command instanceof UpDownType) {
162 moveShade(SECONDARY_ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
163 } else if (command instanceof StopMoveType) {
164 if (StopMoveType.STOP.equals(command)) {
167 logger.warn("Unexpected StopMoveType command");
175 * Update the state of the channels based on the ShadeData provided.
177 * @param shadeData the ShadeData to be used; may be null.
179 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
180 if (shadeData != null) {
181 updateStatus(ThingStatus.ONLINE);
182 updateSoftProperties(shadeData);
183 updateBindingStates(shadeData.positions);
184 updateBatteryLevel(shadeData.batteryStatus);
185 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
186 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
188 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
190 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
195 * Update the Thing's properties based on the contents of the provided ShadeData.
197 * Checks the database of known Shade 'types' and 'capabilities' and logs any unknown or incompatible values, so
198 * that developers can be kept updated about the potential need to add support for that type resp. capabilities.
202 private void updateSoftProperties(ShadeData shadeData) {
203 final Map<String, String> properties = getThing().getProperties();
204 boolean propChanged = false;
206 // update 'type' property
207 final int type = shadeData.type;
208 String propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_TYPE;
209 String propOldVal = properties.getOrDefault(propKey, "");
210 String propNewVal = db.getType(type).toString();
211 if (!propNewVal.equals(propOldVal)) {
213 getThing().setProperty(propKey, propNewVal);
214 if ((type > 0) && !db.isTypeInDatabase(type)) {
215 db.logTypeNotInDatabase(type);
219 // update 'capabilities' property
220 final Integer temp = shadeData.capabilities;
221 final int capabilitiesVal = temp != null ? temp.intValue() : -1;
222 Capabilities capabilities = db.getCapabilities(capabilitiesVal);
223 propKey = HDPowerViewBindingConstants.PROPERTY_SHADE_CAPABILITIES;
224 propOldVal = properties.getOrDefault(propKey, "");
225 propNewVal = capabilities.toString();
226 if (!propNewVal.equals(propOldVal)) {
228 getThing().setProperty(propKey, propNewVal);
229 if ((capabilitiesVal >= 0) && !db.isCapabilitiesInDatabase(capabilitiesVal)) {
230 db.logCapabilitiesNotInDatabase(type, capabilitiesVal);
234 // update shadeCapabilities field
235 if (capabilitiesVal >= 0) {
236 shadeCapabilities = capabilitiesVal;
239 if (propChanged && db.isCapabilitiesInDatabase(capabilitiesVal) && db.isTypeInDatabase(type)
240 && (capabilitiesVal != db.getType(type).getCapabilities())) {
241 db.logCapabilitiesMismatch(type, capabilitiesVal);
246 * After a hard refresh, update the Thing's properties based on the contents of the provided ShadeData.
248 * Checks if the secondary support capabilities in the database of known Shade 'types' and 'capabilities' matches
249 * that implied by the ShadeData and logs any incompatible values, so that developers can be kept updated about the
250 * potential need to add support for that type resp. capabilities.
254 private void updateHardProperties(ShadeData shadeData) {
255 final ShadePosition positions = shadeData.positions;
256 if (positions != null) {
257 final Map<String, String> properties = getThing().getProperties();
259 // update 'jsonHasSecondary' property
260 String propKey = HDPowerViewBindingConstants.PROPERTY_SECONDARY_RAIL_DETECTED;
261 String propOldVal = properties.getOrDefault(propKey, "");
262 boolean propNewBool = positions.secondaryRailDetected();
263 String propNewVal = String.valueOf(propNewBool);
264 if (!propNewVal.equals(propOldVal)) {
265 getThing().setProperty(propKey, propNewVal);
266 final Integer temp = shadeData.capabilities;
267 final int capabilities = temp != null ? temp.intValue() : -1;
268 if (propNewBool != db.getCapabilities(capabilities).supportsSecondary()) {
269 db.logPropertyMismatch(propKey, shadeData.type, capabilities, propNewBool);
273 // update 'jsonTiltAnywhere' property
274 propKey = HDPowerViewBindingConstants.PROPERTY_TILT_ANYWHERE_DETECTED;
275 propOldVal = properties.getOrDefault(propKey, "");
276 propNewBool = positions.tiltAnywhereDetected();
277 propNewVal = String.valueOf(propNewBool);
278 if (!propNewVal.equals(propOldVal)) {
279 getThing().setProperty(propKey, propNewVal);
280 final Integer temp = shadeData.capabilities;
281 final int capabilities = temp != null ? temp.intValue() : -1;
282 if (propNewBool != db.getCapabilities(capabilities).supportsTiltAnywhere()) {
283 db.logPropertyMismatch(propKey, shadeData.type, capabilities, propNewBool);
289 private void updateBindingStates(@Nullable ShadePosition shadePos) {
290 if (shadePos == null) {
291 logger.debug("The value of 'shadePosition' argument was null!");
292 } else if (shadeCapabilities < 0) {
293 logger.debug("The 'shadeCapabilities' field has not been initialized!");
295 Capabilities caps = db.getCapabilities(shadeCapabilities);
296 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(caps, PRIMARY_ZERO_IS_CLOSED));
297 updateState(CHANNEL_SHADE_VANE, shadePos.getState(caps, VANE_TILT_COORDS));
298 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(caps, SECONDARY_ZERO_IS_OPEN));
301 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
302 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
303 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
306 private void updateBatteryLevel(int batteryStatus) {
308 switch (batteryStatus) {
316 case 4: // Plugged in
319 default: // No status available (0) or invalid
320 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
321 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
324 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
325 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
328 private void moveShade(CoordinateSystem coordSys, int newPercent) {
330 HDPowerViewHubHandler bridge;
331 if ((bridge = getBridgeHandler()) == null) {
332 throw new HubProcessingException("Missing bridge handler");
334 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
335 if (webTargets == null) {
336 throw new HubProcessingException("Web targets not initialized");
338 ShadePosition newPosition = null;
339 // (try to) read the positions from the hub
340 int shadeId = getShadeId();
341 Shade shade = webTargets.getShade(shadeId);
343 ShadeData shadeData = shade.shade;
344 if (shadeData != null) {
345 newPosition = shadeData.positions;
348 // if no positions returned, then create a new position
349 if (newPosition == null) {
350 newPosition = new ShadePosition();
352 // set the new position value, and write the positions to the hub
353 webTargets.moveShade(shadeId,
354 newPosition.setPosition(db.getCapabilities(shadeCapabilities), coordSys, newPercent));
355 // update the Channels to match the new position
356 final ShadePosition finalPosition = newPosition;
357 scheduler.submit(() -> {
358 updateBindingStates(finalPosition);
360 } catch (HubProcessingException | NumberFormatException e) {
361 logger.warn("Unexpected error: {}", e.getMessage());
363 } catch (HubMaintenanceException e) {
364 // exceptions are logged in HDPowerViewWebTargets
369 private int getShadeId() throws NumberFormatException {
370 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
372 throw new NumberFormatException("null input string");
374 return Integer.parseInt(str);
377 private void stopShade() {
379 HDPowerViewHubHandler bridge;
380 if ((bridge = getBridgeHandler()) == null) {
381 throw new HubProcessingException("Missing bridge handler");
383 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
384 if (webTargets == null) {
385 throw new HubProcessingException("Web targets not initialized");
387 int shadeId = getShadeId();
388 webTargets.stopShade(shadeId);
389 requestRefreshShadePosition();
390 } catch (HubProcessingException | NumberFormatException e) {
391 logger.warn("Unexpected error: {}", e.getMessage());
393 } catch (HubMaintenanceException e) {
394 // exceptions are logged in HDPowerViewWebTargets
400 * Request that the shade shall undergo a 'hard' refresh for querying its current position
402 protected synchronized void requestRefreshShadePosition() {
403 if (refreshPositionFuture == null) {
404 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
410 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
412 protected synchronized void requestRefreshShadeSurvey() {
413 if (refreshSignalFuture == null) {
414 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, REFRESH_DELAY_SEC, TimeUnit.SECONDS);
419 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
421 protected synchronized void requestRefreshShadeBatteryLevel() {
422 if (refreshBatteryLevelFuture == null) {
423 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
428 private void doRefreshShadePosition() {
429 this.doRefreshShade(RefreshKind.POSITION);
430 refreshPositionFuture = null;
433 private void doRefreshShadeSignal() {
434 this.doRefreshShade(RefreshKind.SURVEY);
435 refreshSignalFuture = null;
438 private void doRefreshShadeBatteryLevel() {
439 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
440 refreshBatteryLevelFuture = null;
443 private void doRefreshShade(RefreshKind kind) {
445 HDPowerViewHubHandler bridge;
446 if ((bridge = getBridgeHandler()) == null) {
447 throw new HubProcessingException("Missing bridge handler");
449 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
450 if (webTargets == null) {
451 throw new HubProcessingException("Web targets not initialized");
453 int shadeId = getShadeId();
457 shade = webTargets.refreshShadePosition(shadeId);
460 Survey survey = webTargets.getShadeSurvey(shadeId);
461 if (survey != null && survey.surveyData != null) {
462 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
464 logger.warn("No response from shade {} survey", shadeId);
468 shade = webTargets.refreshShadeBatteryLevel(shadeId);
471 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
474 ShadeData shadeData = shade.shade;
475 if (shadeData != null) {
476 if (Boolean.TRUE.equals(shadeData.timedOut)) {
477 logger.warn("Shade {} wireless refresh time out", shadeId);
478 } else if (kind == RefreshKind.POSITION) {
479 updateHardProperties(shadeData);
483 } catch (HubProcessingException | NumberFormatException e) {
484 logger.warn("Unexpected error: {}", e.getMessage());
485 } catch (HubMaintenanceException e) {
486 // exceptions are logged in HDPowerViewWebTargets