]> git.basschouten.com Git - openhab-addons.git/blob
502e2c6ff648a42bb49039fea97760fc678a233e
[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.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;
54
55 /**
56  * Handles commands for an HD PowerView Shade
57  *
58  * @author Andy Lintner - Initial contribution
59  * @author Andrew Fiddian-Green - Added support for secondary rail positions
60  */
61 @NonNullByDefault
62 public class HDPowerViewShadeHandler extends AbstractHubbedThingHandler {
63
64     private enum RefreshKind {
65         POSITION,
66         SURVEY,
67         BATTERY_LEVEL
68     }
69
70     private final Logger logger = LoggerFactory.getLogger(HDPowerViewShadeHandler.class);
71
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;
76
77     public HDPowerViewShadeHandler(Thing thing) {
78         super(thing);
79     }
80
81     @Override
82     public void initialize() {
83         try {
84             getShadeId();
85         } catch (NumberFormatException e) {
86             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87                     "@text/offline.conf-error.invalid-id");
88             return;
89         }
90         Bridge bridge = getBridge();
91         if (bridge == null) {
92             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
93             return;
94         }
95         if (!(bridge.getHandler() instanceof HDPowerViewHubHandler)) {
96             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
97                     "@text/offline.conf-error.invalid-bridge-handler");
98             return;
99         }
100         ThingStatus bridgeStatus = bridge.getStatus();
101         if (bridgeStatus == ThingStatus.ONLINE) {
102             updateStatus(ThingStatus.ONLINE);
103         } else {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
105         }
106     }
107
108     @Override
109     public void handleCommand(ChannelUID channelUID, Command command) {
110         String channelId = channelUID.getId();
111
112         if (RefreshType.REFRESH == command) {
113             switch (channelId) {
114                 case CHANNEL_SHADE_POSITION:
115                 case CHANNEL_SHADE_SECONDARY_POSITION:
116                 case CHANNEL_SHADE_VANE:
117                     requestRefreshShadePosition();
118                     break;
119                 case CHANNEL_SHADE_LOW_BATTERY:
120                 case CHANNEL_SHADE_BATTERY_LEVEL:
121                 case CHANNEL_SHADE_BATTERY_VOLTAGE:
122                     requestRefreshShadeBatteryLevel();
123                     break;
124                 case CHANNEL_SHADE_SIGNAL_STRENGTH:
125                     requestRefreshShadeSurvey();
126                     break;
127             }
128             return;
129         }
130
131         switch (channelId) {
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)) {
139                         stopShade();
140                     } else {
141                         logger.warn("Unexpected StopMoveType command");
142                     }
143                 }
144                 break;
145
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);
151                 }
152                 break;
153
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)) {
161                         stopShade();
162                     } else {
163                         logger.warn("Unexpected StopMoveType command");
164                     }
165                 }
166                 break;
167         }
168     }
169
170     /**
171      * Update the state of the channels based on the ShadeData provided
172      *
173      * @param shadeData the ShadeData to be used; may be null
174      */
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)
182                             : UnDefType.UNDEF);
183             updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
184         } else {
185             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
186         }
187     }
188
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));
194         } else {
195             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
196             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
197             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
198         }
199     }
200
201     private void updateBatteryLevel(int batteryStatus) {
202         int mappedValue;
203         switch (batteryStatus) {
204             case 1: // Low
205                 mappedValue = 10;
206                 break;
207             case 2: // Medium
208                 mappedValue = 50;
209                 break;
210             case 3: // High
211             case 4: // Plugged in
212                 mappedValue = 100;
213                 break;
214             default: // No status available (0) or invalid
215                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
216                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
217                 return;
218         }
219         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
220         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
221     }
222
223     private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
224         try {
225             HDPowerViewHubHandler bridge;
226             if ((bridge = getBridgeHandler()) == null) {
227                 throw new HubProcessingException("Missing bridge handler");
228             }
229             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
230             if (webTargets == null) {
231                 throw new HubProcessingException("Web targets not initialized");
232             }
233             int shadeId = getShadeId();
234
235             switch (actuatorClass) {
236                 case PRIMARY_ACTUATOR:
237                     // write the new primary position
238                     webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
239                     break;
240                 case SECONDARY_ACTUATOR:
241                     // read the current primary position; default value 100%
242                     int primaryPercent = 100;
243                     Shade shade = webTargets.getShade(shadeId);
244                     if (shade != null) {
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();
252                                 }
253                             }
254                         }
255                     }
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));
259             }
260         } catch (HubProcessingException | NumberFormatException e) {
261             logger.warn("Unexpected error: {}", e.getMessage());
262             return;
263         } catch (HubMaintenanceException e) {
264             // exceptions are logged in HDPowerViewWebTargets
265             return;
266         }
267     }
268
269     private int getShadeId() throws NumberFormatException {
270         String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
271         if (str == null) {
272             throw new NumberFormatException("null input string");
273         }
274         return Integer.parseInt(str);
275     }
276
277     private void stopShade() {
278         try {
279             HDPowerViewHubHandler bridge;
280             if ((bridge = getBridgeHandler()) == null) {
281                 throw new HubProcessingException("Missing bridge handler");
282             }
283             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
284             if (webTargets == null) {
285                 throw new HubProcessingException("Web targets not initialized");
286             }
287             int shadeId = getShadeId();
288             webTargets.stopShade(shadeId);
289             requestRefreshShadePosition();
290         } catch (HubProcessingException | NumberFormatException e) {
291             logger.warn("Unexpected error: {}", e.getMessage());
292             return;
293         } catch (HubMaintenanceException e) {
294             // exceptions are logged in HDPowerViewWebTargets
295             return;
296         }
297     }
298
299     /**
300      * Request that the shade shall undergo a 'hard' refresh for querying its current position
301      */
302     protected synchronized void requestRefreshShadePosition() {
303         if (refreshPositionFuture == null) {
304             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
305                     TimeUnit.SECONDS);
306         }
307     }
308
309     /**
310      * Request that the shade shall undergo a 'hard' refresh for querying its survey data
311      */
312     protected synchronized void requestRefreshShadeSurvey() {
313         if (refreshSignalFuture == null) {
314             refreshSignalFuture = scheduler.schedule(this::doRefreshShadeSignal, REFRESH_DELAY_SEC, TimeUnit.SECONDS);
315         }
316     }
317
318     /**
319      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
320      */
321     protected synchronized void requestRefreshShadeBatteryLevel() {
322         if (refreshBatteryLevelFuture == null) {
323             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
324                     TimeUnit.SECONDS);
325         }
326     }
327
328     private void doRefreshShadePosition() {
329         this.doRefreshShade(RefreshKind.POSITION);
330         refreshPositionFuture = null;
331     }
332
333     private void doRefreshShadeSignal() {
334         this.doRefreshShade(RefreshKind.SURVEY);
335         refreshSignalFuture = null;
336     }
337
338     private void doRefreshShadeBatteryLevel() {
339         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
340         refreshBatteryLevelFuture = null;
341     }
342
343     private void doRefreshShade(RefreshKind kind) {
344         try {
345             HDPowerViewHubHandler bridge;
346             if ((bridge = getBridgeHandler()) == null) {
347                 throw new HubProcessingException("Missing bridge handler");
348             }
349             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
350             if (webTargets == null) {
351                 throw new HubProcessingException("Web targets not initialized");
352             }
353             int shadeId = getShadeId();
354             Shade shade;
355             switch (kind) {
356                 case POSITION:
357                     shade = webTargets.refreshShadePosition(shadeId);
358                     break;
359                 case SURVEY:
360                     Survey survey = webTargets.getShadeSurvey(shadeId);
361                     if (survey != null && survey.surveyData != null) {
362                         logger.debug("Survey response for shade {}: {}", survey.shadeId, survey.toString());
363                     } else {
364                         logger.warn("No response from shade {} survey", shadeId);
365                     }
366                     return;
367                 case BATTERY_LEVEL:
368                     shade = webTargets.refreshShadeBatteryLevel(shadeId);
369                     break;
370                 default:
371                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
372             }
373             if (shade != null) {
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);
378                     }
379                 }
380             }
381         } catch (HubProcessingException | NumberFormatException e) {
382             logger.warn("Unexpected error: {}", e.getMessage());
383         } catch (HubMaintenanceException e) {
384             // exceptions are logged in HDPowerViewWebTargets
385         }
386     }
387 }