]> git.basschouten.com Git - openhab-addons.git/blob
e996b58ce33d5f8815b231d5bea9b1d138877dda
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.hdpowerview.internal.handler;
14
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.*;
18
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import javax.ws.rs.NotSupportedException;
23
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;
53
54 /**
55  * Handles commands for an HD PowerView Shade
56  *
57  * @author Andy Lintner - Initial contribution
58  * @author Andrew Fiddian-Green - Added support for secondary rail positions
59  */
60 @NonNullByDefault
61 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
62
63     private enum RefreshKind {
64         POSITION,
65         BATTERY_LEVEL
66     }
67
68     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
69
70     private static final int REFRESH_DELAY_SEC = 10;
71     private @Nullable ScheduledFuture<?> refreshPositionFuture = null;
72     private @Nullable ScheduledFuture<?> refreshBatteryLevelFuture = null;
73
74     public HDPowerViewShadeHandler(Thing thing) {
75         super(thing);
76     }
77
78     @Override
79     public void initialize() {
80         try {
81             getShadeId();
82         } catch (NumberFormatException e) {
83             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
84                     "@text/offline.conf-error.invalid-id");
85             return;
86         }
87         Bridge bridge = getBridge();
88         if (bridge == null) {
89             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
90             return;
91         }
92         if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
93             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
94                     "@text/offline.conf-error.invalid-bridge-handler");
95             return;
96         }
97         ThingStatus bridgeStatus = bridge.getStatus();
98         if (bridgeStatus == ThingStatus.ONLINE) {
99             updateStatus(ThingStatus.ONLINE);
100         } else {
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
102         }
103     }
104
105     @Override
106     public void handleCommand(ChannelUID channelUID, Command command) {
107         String channelId = channelUID.getId();
108
109         if (RefreshType.REFRESH == command) {
110             switch (channelId) {
111                 case CHANNEL_SHADE_POSITION:
112                 case CHANNEL_SHADE_SECONDARY_POSITION:
113                 case CHANNEL_SHADE_VANE:
114                     requestRefreshShadePosition();
115                     break;
116                 case CHANNEL_SHADE_LOW_BATTERY:
117                 case CHANNEL_SHADE_BATTERY_LEVEL:
118                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
119                     requestRefreshShadeBatteryLevel();
120                     break;
121             }
122             return;
123         }
124
125         switch (channelId) {
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)) {
133                         stopShade();
134                     } else {
135                         logger.warn("Unexpected StopMoveType command");
136                     }
137                 }
138                 break;
139
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);
145                 }
146                 break;
147
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)) {
155                         stopShade();
156                     } else {
157                         logger.warn("Unexpected StopMoveType command");
158                     }
159                 }
160                 break;
161         }
162     }
163
164     /**
165      * Update the state of the channels based on the ShadeData provided
166      *
167      * @param shadeData the ShadeData to be used; may be null
168      */
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)
176                             : UnDefType.UNDEF);
177             updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
178         } else {
179             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
180         }
181     }
182
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));
188         } else {
189             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
190             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
191             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
192         }
193     }
194
195     private void updateBatteryLevel(int batteryStatus) {
196         int mappedValue;
197         switch (batteryStatus) {
198             case 1: // Low
199                 mappedValue = 10;
200                 break;
201             case 2: // Medium
202                 mappedValue = 50;
203                 break;
204             case 3: // High
205             case 4: // Plugged in
206                 mappedValue = 100;
207                 break;
208             default: // No status available (0) or invalid
209                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
210                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
211                 return;
212         }
213         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
214         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
215     }
216
217     private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
218         try {
219             HDPowerViewHubHandler bridge;
220             if ((bridge = getBridgeHandler()) == null) {
221                 throw new HubProcessingException("Missing bridge handler");
222             }
223             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
224             if (webTargets == null) {
225                 throw new HubProcessingException("Web targets not initialized");
226             }
227             int shadeId = getShadeId();
228
229             switch (actuatorClass) {
230                 case PRIMARY_ACTUATOR:
231                     // write the new primary position
232                     webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
233                     break;
234                 case SECONDARY_ACTUATOR:
235                     // read the current primary position; default value 100%
236                     int primaryPercent = 100;
237                     Shade shade = webTargets.getShade(shadeId);
238                     if (shade != null) {
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();
246                                 }
247                             }
248                         }
249                     }
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));
253             }
254         } catch (HubProcessingException | NumberFormatException e) {
255             logger.warn("Unexpected error: {}", e.getMessage());
256             return;
257         } catch (HubMaintenanceException e) {
258             // exceptions are logged in HDPowerViewWebTargets
259             return;
260         }
261     }
262
263     private int getShadeId() throws NumberFormatException {
264         String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
265         if (str == null) {
266             throw new NumberFormatException("null input string");
267         }
268         return Integer.parseInt(str);
269     }
270
271     private void stopShade() {
272         try {
273             HDPowerViewHubHandler bridge;
274             if ((bridge = getBridgeHandler()) == null) {
275                 throw new HubProcessingException("Missing bridge handler");
276             }
277             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
278             if (webTargets == null) {
279                 throw new HubProcessingException("Web targets not initialized");
280             }
281             int shadeId = getShadeId();
282             webTargets.stopShade(shadeId);
283             requestRefreshShadePosition();
284         } catch (HubProcessingException | NumberFormatException e) {
285             logger.warn("Unexpected error: {}", e.getMessage());
286             return;
287         } catch (HubMaintenanceException e) {
288             // exceptions are logged in HDPowerViewWebTargets
289             return;
290         }
291     }
292
293     /**
294      * Request that the shade shall undergo a 'hard' refresh for querying its current position
295      */
296     protected synchronized void requestRefreshShadePosition() {
297         if (refreshPositionFuture == null) {
298             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
299                     TimeUnit.SECONDS);
300         }
301     }
302
303     /**
304      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
305      */
306     protected synchronized void requestRefreshShadeBatteryLevel() {
307         if (refreshBatteryLevelFuture == null) {
308             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
309                     TimeUnit.SECONDS);
310         }
311     }
312
313     private void doRefreshShadePosition() {
314         this.doRefreshShade(RefreshKind.POSITION);
315         refreshPositionFuture = null;
316     }
317
318     private void doRefreshShadeBatteryLevel() {
319         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
320         refreshBatteryLevelFuture = null;
321     }
322
323     private void doRefreshShade(RefreshKind kind) {
324         try {
325             HDPowerViewHubHandler bridge;
326             if ((bridge = getBridgeHandler()) == null) {
327                 throw new HubProcessingException("Missing bridge handler");
328             }
329             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
330             if (webTargets == null) {
331                 throw new HubProcessingException("Web targets not initialized");
332             }
333             int shadeId = getShadeId();
334             Shade shade;
335             switch (kind) {
336                 case POSITION:
337                     shade = webTargets.refreshShadePosition(shadeId);
338                     break;
339                 case BATTERY_LEVEL:
340                     shade = webTargets.refreshShadeBatteryLevel(shadeId);
341                     break;
342                 default:
343                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
344             }
345             if (shade != null) {
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);
350                     }
351                 }
352             }
353         } catch (HubProcessingException | NumberFormatException e) {
354             logger.warn("Unexpected error: {}", e.getMessage());
355         } catch (HubMaintenanceException e) {
356             // exceptions are logged in HDPowerViewWebTargets
357         }
358     }
359 }