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.config.HDPowerViewShadeConfiguration;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.PercentType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StopMoveType;
40 import org.openhab.core.library.types.UpDownType;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.State;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * Handles commands for an HD PowerView Shade
57 * @author Andy Lintner - Initial contribution
58 * @author Andrew Fiddian-Green - Added support for secondary rail positions
61 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
63 private enum RefreshKind {
68 private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
70 private static final int REFRESH_DELAY_SEC = 10;
71 private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
72 private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
74 public HDPowerViewShadeHandler(Thing thing) {
79 public void initialize() {
82 } catch (NumberFormatException e) {
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
84 "@text/offline.conf-error.invalid-id");
87 Bridge bridge = getBridge();
89 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
92 if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
94 "@text/offline.conf-error.invalid-bridge-handler");
97 ThingStatus bridgeStatus = bridge.getStatus();
98 if (bridgeStatus == ThingStatus.ONLINE) {
99 updateStatus(ThingStatus.ONLINE);
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
106 public void handleCommand(ChannelUID channelUID, Command command) {
107 if (RefreshType.REFRESH.equals(command)) {
108 requestRefreshShadePosition();
112 switch (channelUID.getId()) {
113 case CHANNEL_SHADE_POSITION:
114 if (command instanceof PercentType) {
115 moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, ((PercentType) command).intValue());
116 } else if (command instanceof UpDownType) {
117 moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
118 } else if (command instanceof StopMoveType) {
119 if (StopMoveType.STOP.equals(command)) {
122 logger.warn("Unexpected StopMoveType command");
127 case CHANNEL_SHADE_VANE:
128 if (command instanceof PercentType) {
129 moveShade(PRIMARY_ACTUATOR, VANE_COORDS, ((PercentType) command).intValue());
130 } else if (command instanceof OnOffType) {
131 moveShade(PRIMARY_ACTUATOR, VANE_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
135 case CHANNEL_SHADE_SECONDARY_POSITION:
136 if (command instanceof PercentType) {
137 moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, ((PercentType) command).intValue());
138 } else if (command instanceof UpDownType) {
139 moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
140 } else if (command instanceof StopMoveType) {
141 if (StopMoveType.STOP.equals(command)) {
144 logger.warn("Unexpected StopMoveType command");
152 * Update the state of the channels based on the ShadeData provided
154 * @param shadeData the ShadeData to be used; may be null
156 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
157 if (shadeData != null) {
158 updateStatus(ThingStatus.ONLINE);
159 updateBindingStates(shadeData.positions);
160 updateBatteryLevel(shadeData.batteryStatus);
161 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
162 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
164 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
170 private void updateBindingStates(@Nullable ShadePosition shadePos) {
171 if (shadePos != null) {
172 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED));
173 updateState(CHANNEL_SHADE_VANE, shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS));
174 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN));
176 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
177 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
178 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
182 private void updateBatteryLevel(int batteryStatus) {
184 switch (batteryStatus) {
192 case 4: // Plugged in
195 default: // No status available (0) or invalid
196 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
197 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
200 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
201 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
204 private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
206 HDPowerViewHubHandler bridge;
207 if ((bridge = getBridgeHandler()) == null) {
208 throw new HubProcessingException("Missing bridge handler");
210 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
211 if (webTargets == null) {
212 throw new HubProcessingException("Web targets not initialized");
214 int shadeId = getShadeId();
216 switch (actuatorClass) {
217 case PRIMARY_ACTUATOR:
218 // write the new primary position
219 webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
221 case SECONDARY_ACTUATOR:
222 // read the current primary position; default value 100%
223 int primaryPercent = 100;
224 Shade shade = webTargets.getShade(shadeId);
226 ShadeData shadeData = shade.shade;
227 if (shadeData != null) {
228 ShadePosition shadePos = shadeData.positions;
229 if (shadePos != null) {
230 State primaryState = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
231 if (primaryState instanceof PercentType) {
232 primaryPercent = ((PercentType) primaryState).intValue();
237 // write the current primary position, plus the new secondary position
238 webTargets.moveShade(shadeId,
239 ShadePosition.create(ZERO_IS_CLOSED, primaryPercent, ZERO_IS_OPEN, newPercent));
241 } catch (HubProcessingException | NumberFormatException e) {
242 logger.warn("Unexpected error: {}", e.getMessage());
244 } catch (HubMaintenanceException e) {
245 // exceptions are logged in HDPowerViewWebTargets
250 private int getShadeId() throws NumberFormatException {
251 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
253 throw new NumberFormatException("null input string");
255 return Integer.parseInt(str);
258 private void stopShade() {
260 HDPowerViewHubHandler bridge;
261 if ((bridge = getBridgeHandler()) == null) {
262 throw new HubProcessingException("Missing bridge handler");
264 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
265 if (webTargets == null) {
266 throw new HubProcessingException("Web targets not initialized");
268 int shadeId = getShadeId();
269 webTargets.stopShade(shadeId);
270 requestRefreshShadePosition();
271 } catch (HubProcessingException | NumberFormatException e) {
272 logger.warn("Unexpected error: {}", e.getMessage());
274 } catch (HubMaintenanceException e) {
275 // exceptions are logged in HDPowerViewWebTargets
281 * Request that the shade shall undergo a 'hard' refresh for querying its current position
283 protected synchronized void requestRefreshShadePosition() {
284 if (refreshPositionFuture == null) {
285 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
291 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
293 protected synchronized void requestRefreshShadeBatteryLevel() {
294 if (refreshBatteryLevelFuture == null) {
295 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
300 private void doRefreshShadePosition() {
301 this.doRefreshShade(RefreshKind.POSITION);
302 refreshPositionFuture = null;
305 private void doRefreshShadeBatteryLevel() {
306 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
307 refreshBatteryLevelFuture = null;
310 private void doRefreshShade(RefreshKind kind) {
312 HDPowerViewHubHandler bridge;
313 if ((bridge = getBridgeHandler()) == null) {
314 throw new HubProcessingException("Missing bridge handler");
316 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
317 if (webTargets == null) {
318 throw new HubProcessingException("Web targets not initialized");
320 int shadeId = getShadeId();
324 shade = webTargets.refreshShadePosition(shadeId);
327 shade = webTargets.refreshShadeBatteryLevel(shadeId);
330 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
333 ShadeData shadeData = shade.shade;
334 if (shadeData != null) {
335 if (Boolean.TRUE.equals(shadeData.timedOut)) {
336 logger.warn("Shade {} wireless refresh time out", shadeId);
340 } catch (HubProcessingException | NumberFormatException e) {
341 logger.warn("Unexpected error: {}", e.getMessage());
342 } catch (HubMaintenanceException e) {
343 // exceptions are logged in HDPowerViewWebTargets