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 String channelId = channelUID.getId();
109 if (RefreshType.REFRESH == command) {
111 case CHANNEL_SHADE_POSITION:
112 case CHANNEL_SHADE_SECONDARY_POSITION:
113 case CHANNEL_SHADE_VANE:
114 requestRefreshShadePosition();
116 case CHANNEL_SHADE_LOW_BATTERY:
117 case CHANNEL_SHADE_BATTERY_LEVEL:
118 case CHANNEL_SHADE_BATTERY_VOLTAGE:
119 requestRefreshShadeBatteryLevel();
126 case CHANNEL_SHADE_POSITION:
127 if (command instanceof PercentType) {
128 moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, ((PercentType) command).intValue());
129 } else if (command instanceof UpDownType) {
130 moveShade(PRIMARY_ACTUATOR, ZERO_IS_CLOSED, UpDownType.UP.equals(command) ? 0 : 100);
131 } else if (command instanceof StopMoveType) {
132 if (StopMoveType.STOP.equals(command)) {
135 logger.warn("Unexpected StopMoveType command");
140 case CHANNEL_SHADE_VANE:
141 if (command instanceof PercentType) {
142 moveShade(PRIMARY_ACTUATOR, VANE_COORDS, ((PercentType) command).intValue());
143 } else if (command instanceof OnOffType) {
144 moveShade(PRIMARY_ACTUATOR, VANE_COORDS, OnOffType.ON.equals(command) ? 100 : 0);
148 case CHANNEL_SHADE_SECONDARY_POSITION:
149 if (command instanceof PercentType) {
150 moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, ((PercentType) command).intValue());
151 } else if (command instanceof UpDownType) {
152 moveShade(SECONDARY_ACTUATOR, ZERO_IS_OPEN, UpDownType.UP.equals(command) ? 0 : 100);
153 } else if (command instanceof StopMoveType) {
154 if (StopMoveType.STOP.equals(command)) {
157 logger.warn("Unexpected StopMoveType command");
165 * Update the state of the channels based on the ShadeData provided
167 * @param shadeData the ShadeData to be used; may be null
169 protected void onReceiveUpdate(@Nullable ShadeData shadeData) {
170 if (shadeData != null) {
171 updateStatus(ThingStatus.ONLINE);
172 updateBindingStates(shadeData.positions);
173 updateBatteryLevel(shadeData.batteryStatus);
174 updateState(CHANNEL_SHADE_BATTERY_VOLTAGE,
175 shadeData.batteryStrength > 0 ? new QuantityType<>(shadeData.batteryStrength / 10, Units.VOLT)
177 updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
183 private void updateBindingStates(@Nullable ShadePosition shadePos) {
184 if (shadePos != null) {
185 updateState(CHANNEL_SHADE_POSITION, shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED));
186 updateState(CHANNEL_SHADE_VANE, shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS));
187 updateState(CHANNEL_SHADE_SECONDARY_POSITION, shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN));
189 updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
190 updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
191 updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
195 private void updateBatteryLevel(int batteryStatus) {
197 switch (batteryStatus) {
205 case 4: // Plugged in
208 default: // No status available (0) or invalid
209 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
210 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
213 updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
214 updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
217 private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
219 HDPowerViewHubHandler bridge;
220 if ((bridge = getBridgeHandler()) == null) {
221 throw new HubProcessingException("Missing bridge handler");
223 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
224 if (webTargets == null) {
225 throw new HubProcessingException("Web targets not initialized");
227 int shadeId = getShadeId();
229 switch (actuatorClass) {
230 case PRIMARY_ACTUATOR:
231 // write the new primary position
232 webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
234 case SECONDARY_ACTUATOR:
235 // read the current primary position; default value 100%
236 int primaryPercent = 100;
237 Shade shade = webTargets.getShade(shadeId);
239 ShadeData shadeData = shade.shade;
240 if (shadeData != null) {
241 ShadePosition shadePos = shadeData.positions;
242 if (shadePos != null) {
243 State primaryState = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
244 if (primaryState instanceof PercentType) {
245 primaryPercent = ((PercentType) primaryState).intValue();
250 // write the current primary position, plus the new secondary position
251 webTargets.moveShade(shadeId,
252 ShadePosition.create(ZERO_IS_CLOSED, primaryPercent, ZERO_IS_OPEN, newPercent));
254 } catch (HubProcessingException | NumberFormatException e) {
255 logger.warn("Unexpected error: {}", e.getMessage());
257 } catch (HubMaintenanceException e) {
258 // exceptions are logged in HDPowerViewWebTargets
263 private int getShadeId() throws NumberFormatException {
264 String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
266 throw new NumberFormatException("null input string");
268 return Integer.parseInt(str);
271 private void stopShade() {
273 HDPowerViewHubHandler bridge;
274 if ((bridge = getBridgeHandler()) == null) {
275 throw new HubProcessingException("Missing bridge handler");
277 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
278 if (webTargets == null) {
279 throw new HubProcessingException("Web targets not initialized");
281 int shadeId = getShadeId();
282 webTargets.stopShade(shadeId);
283 requestRefreshShadePosition();
284 } catch (HubProcessingException | NumberFormatException e) {
285 logger.warn("Unexpected error: {}", e.getMessage());
287 } catch (HubMaintenanceException e) {
288 // exceptions are logged in HDPowerViewWebTargets
294 * Request that the shade shall undergo a 'hard' refresh for querying its current position
296 protected synchronized void requestRefreshShadePosition() {
297 if (refreshPositionFuture == null) {
298 refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
304 * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
306 protected synchronized void requestRefreshShadeBatteryLevel() {
307 if (refreshBatteryLevelFuture == null) {
308 refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
313 private void doRefreshShadePosition() {
314 this.doRefreshShade(RefreshKind.POSITION);
315 refreshPositionFuture = null;
318 private void doRefreshShadeBatteryLevel() {
319 this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
320 refreshBatteryLevelFuture = null;
323 private void doRefreshShade(RefreshKind kind) {
325 HDPowerViewHubHandler bridge;
326 if ((bridge = getBridgeHandler()) == null) {
327 throw new HubProcessingException("Missing bridge handler");
329 HDPowerViewWebTargets webTargets = bridge.getWebTargets();
330 if (webTargets == null) {
331 throw new HubProcessingException("Web targets not initialized");
333 int shadeId = getShadeId();
337 shade = webTargets.refreshShadePosition(shadeId);
340 shade = webTargets.refreshShadeBatteryLevel(shadeId);
343 throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
346 ShadeData shadeData = shade.shade;
347 if (shadeData != null) {
348 if (Boolean.TRUE.equals(shadeData.timedOut)) {
349 logger.warn("Shade {} wireless refresh time out", shadeId);
353 } catch (HubProcessingException | NumberFormatException e) {
354 logger.warn("Unexpected error: {}", e.getMessage());
355 } catch (HubMaintenanceException e) {
356 // exceptions are logged in HDPowerViewWebTargets