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.ActuatorClass.*;
17 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.HDPowerViewWebTargets;
27 import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
28 import org.openhab.binding.hdpowerview.internal.HubProcessingException;
29 import org.openhab.binding.hdpowerview.internal.api.ActuatorClass;
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.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StopMoveType;
41 import org.openhab.core.library.types.UpDownType;
42 import org.openhab.core.library.unit.Units;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.State;
51 import org.openhab.core.types.UnDefType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * Handles commands for an HD PowerView Shade
58 * @author Andy Lintner - Initial contribution
59 * @author Andrew Fiddian-Green - Added support for secondary rail positions
62 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
64 private enum RefreshKind {
70 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
72 private static final int REFRESH_DELAY_SEC = 10;
73 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
74 private @Nullable ScheduledFuture<?> refreshSignalFuture = null;
75 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
77 public HDPowerViewShadeHandler(Thing thing) {
82 public void initialize() {
85 } catch (NumberFormatException e) {
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87 "@text/offline.conf-error.invalid-id");
90 Bridge bridge = getBridge();
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
95 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
97 "@text/offline.conf-error.invalid-bridge-handler");
100 ThingStatus bridgeStatus = bridge.getStatus();
101 if (bridgeStatus == ThingStatus.ONLINE) {
102 updateStatus(ThingStatus.ONLINE);
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
109 public void handleCommand(ChannelUID channelUID, Command command) {
110 String channelId = channelUID.getId();
112 if (RefreshType.REFRESH == command) {
114 case CHANNEL_SHADE_POSITION:
115 case CHANNEL_SHADE_SECONDARY_POSITION:
116 case CHANNEL_SHADE_VANE:
117 requestRefreshShadePosition();
119 case CHANNEL_SHADE_LOW_BATTERY:
120 case CHANNEL_SHADE_BATTERY_LEVEL:
121 case CHANNEL_SHADE_BATTERY_VOLTAGE:
122 requestRefreshShadeBatteryLevel();
124 case CHANNEL_SHADE_SIGNAL_STRENGTH:
125 requestRefreshShadeSurvey();
132 case CHANNEL_SHADE_POSITION:
133 if (command instanceof PercentType) {
134 moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, ((PercentType) command).intValue());
135 } else if (command instanceof UpDownType) {
136 moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
137 } else if (command instanceof StopMoveType) {
138 if (StopMoveType.STOP.equals(command)) {
141 logger.warn("Unexpected StopMoveType command");
146 case CHANNEL_SHADE_VANE:
147 if (command instanceof PercentType) {
148 moveShade(PRIMARY_ACTUATOR, VANE_COORDS, ((PercentType) command).intValue());
149 } else if (command instanceof OnOffType) {
150 moveShade(PRIMARY_ACTUATOR, VANE_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
154 case CHANNEL_SHADE_SECONDARY_POSITION:
155 if (command instanceof PercentType) {
156 moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, ((PercentType) command).intValue());
157 } else if (command instanceof UpDownType) {
158 moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
159 } else if (command instanceof StopMoveType) {
160 if (StopMoveType.STOP.equals(command)) {
163 logger.warn("Unexpected StopMoveType command");
171 * Update the state of the channels based on the ShadeData provided
173 * @param shadeData the ShadeData to be used; may be null
175 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
176 if (shadeData != null) {
177 updateStatus(ThingStatus.ONLINE);
178 updateBindingStates(shadeData.positions);
179 updateBatteryLevel(shadeData.batteryStatus);
180 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
181 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
183 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
189 private void updateBindingStates(@Nullable ShadePosition shadePos) {
190 if (shadePos != null) {
191 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED));
192 updateState(CHANNEL_SHADE_VANE, shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS));
193 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN));
195 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
196 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
197 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
201 private void updateBatteryLevel(int batteryStatus) {
203 switch (batteryStatus) {
211 case 4: // Plugged in
214 default: // No status available (0) or invalid
215 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
216 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
219 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
220 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
223 private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
225 HDPowerViewHubHandler bridge;
226 if ((bridge = getBridgeHandler()) == null) {
227 throw new HubProcessingException("Missing bridge handler");
229 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
230 if (webTargets == null) {
231 throw new HubProcessingException("Web targets not initialized");
233 int shadeId = getShadeId();
235 switch (actuatorClass) {
236 case PRIMARY_ACTUATOR:
237 // write the new primary position
238 webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
240 case SECONDARY_ACTUATOR:
241 // read the current primary position; default value 100%
242 int primaryPercent = 100;
243 Shade shade = webTargets.getShade(shadeId);
245 ShadeData shadeData = shade.shade;
246 if (shadeData != null) {
247 ShadePosition shadePos = shadeData.positions;
248 if (shadePos != null) {
249 State primaryState = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
250 if (primaryState instanceof PercentType) {
251 primaryPercent = ((PercentType) primaryState).intValue();
256 // write the current primary position, plus the new secondary position
257 webTargets.moveShade(shadeId,
258 ShadePosition.create(ZERO_IS_CLOSED, primaryPercent, ZERO_IS_OPEN, newPercent));
260 } catch (HubProcessingException | NumberFormatException e) {
261 logger.warn("Unexpected error: {}", e.getMessage());
263 } catch (HubMaintenanceException e) {
264 // exceptions are logged in HDPowerViewWebTargets
269 private int getShadeId() throws NumberFormatException {
270 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
272 throw new NumberFormatException("null input string");
274 return Integer.parseInt(str);
277 private void stopShade() {
279 HDPowerViewHubHandler bridge;
280 if ((bridge = getBridgeHandler()) == null) {
281 throw new HubProcessingException("Missing bridge handler");
283 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
284 if (webTargets == null) {
285 throw new HubProcessingException("Web targets not initialized");
287 int shadeId = getShadeId();
288 webTargets.stopShade(shadeId);
289 requestRefreshShadePosition();
290 } catch (HubProcessingException | NumberFormatException e) {
291 logger.warn("Unexpected error: {}", e.getMessage());
293 } catch (HubMaintenanceException e) {
294 // exceptions are logged in HDPowerViewWebTargets
300 * Request that the shade shall undergo a 'hard' refresh for querying its current position
302 protected synchronized void requestRefreshShadePosition() {
303 if (refreshPositionFuture == null) {
304 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
310 * Request that the shade shall undergo a 'hard' refresh for querying its survey data
312 protected synchronized void requestRefreshShadeSurvey() {
313 if (refreshSignalFuture == null) {
314 refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, REFRESH_DELAY_SEC, TimeUnit.SECONDS);
319 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
321 protected synchronized void requestRefreshShadeBatteryLevel() {
322 if (refreshBatteryLevelFuture == null) {
323 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
328 private void doRefreshShadePosition() {
329 this.doRefreshShade(RefreshKind.POSITION);
330 refreshPositionFuture = null;
333 private void doRefreshShadeSignal() {
334 this.doRefreshShade(RefreshKind.SURVEY);
335 refreshSignalFuture = null;
338 private void doRefreshShadeBatteryLevel() {
339 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
340 refreshBatteryLevelFuture = null;
343 private void doRefreshShade(RefreshKind kind) {
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 int shadeId = getShadeId();
357 shade = webTargets.refreshShadePosition(shadeId);
360 Survey survey = webTargets.getShadeSurvey(shadeId);
361 if (survey != null && survey.surveyData != null) {
362 logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
364 logger.warn("No response from shade {} survey", shadeId);
368 shade = webTargets.refreshShadeBatteryLevel(shadeId);
371 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
374 ShadeData shadeData = shade.shade;
375 if (shadeData != null) {
376 if (Boolean.TRUE.equals(shadeData.timedOut)) {
377 logger.warn("Shade {} wireless refresh time out", shadeId);
381 } catch (HubProcessingException | NumberFormatException e) {
382 logger.warn("Unexpected error: {}", e.getMessage());
383 } catch (HubMaintenanceException e) {
384 // exceptions are logged in HDPowerViewWebTargets