]> git.basschouten.com Git - openhab-addons.git/blob
038b4f71a370a3722f8823e5c5960303eeb233ea
[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         if (RefreshType.REFRESH.equals(command)) {
108             requestRefreshShadePosition();
109             return;
110         }
111
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)) {
120                         stopShade();
121                     } else {
122                         logger.warn("Unexpected StopMoveType command");
123                     }
124                 }
125                 break;
126
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);
132                 }
133                 break;
134
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)) {
142                         stopShade();
143                     } else {
144                         logger.warn("Unexpected StopMoveType command");
145                     }
146                 }
147                 break;
148         }
149     }
150
151     /**
152      * Update the state of the channels based on the ShadeData provided
153      *
154      * @param shadeData the ShadeData to be used; may be null
155      */
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)
163                             : UnDefType.UNDEF);
164             updateState(CHANNEL_SHADE_SIGNAL_STRENGTH, new DecimalType(shadeData.signalStrength));
165         } else {
166             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
167         }
168     }
169
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));
175         } else {
176             updateState(CHANNEL_SHADE_POSITION, UnDefType.UNDEF);
177             updateState(CHANNEL_SHADE_VANE, UnDefType.UNDEF);
178             updateState(CHANNEL_SHADE_SECONDARY_POSITION, UnDefType.UNDEF);
179         }
180     }
181
182     private void updateBatteryLevel(int batteryStatus) {
183         int mappedValue;
184         switch (batteryStatus) {
185             case 1: // Low
186                 mappedValue = 10;
187                 break;
188             case 2: // Medium
189                 mappedValue = 50;
190                 break;
191             case 3: // High
192             case 4: // Plugged in
193                 mappedValue = 100;
194                 break;
195             default: // No status available (0) or invalid
196                 updateState(CHANNEL_SHADE_LOW_BATTERY, UnDefType.UNDEF);
197                 updateState(CHANNEL_SHADE_BATTERY_LEVEL, UnDefType.UNDEF);
198                 return;
199         }
200         updateState(CHANNEL_SHADE_LOW_BATTERY, batteryStatus == 1 ? OnOffType.ON : OnOffType.OFF);
201         updateState(CHANNEL_SHADE_BATTERY_LEVEL, new DecimalType(mappedValue));
202     }
203
204     private void moveShade(ActuatorClass actuatorClass, CoordinateSystem coordSys, int newPercent) {
205         try {
206             HDPowerViewHubHandler bridge;
207             if ((bridge = getBridgeHandler()) == null) {
208                 throw new HubProcessingException("Missing bridge handler");
209             }
210             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
211             if (webTargets == null) {
212                 throw new HubProcessingException("Web targets not initialized");
213             }
214             int shadeId = getShadeId();
215
216             switch (actuatorClass) {
217                 case PRIMARY_ACTUATOR:
218                     // write the new primary position
219                     webTargets.moveShade(shadeId, ShadePosition.create(coordSys, newPercent));
220                     break;
221                 case SECONDARY_ACTUATOR:
222                     // read the current primary position; default value 100%
223                     int primaryPercent = 100;
224                     Shade shade = webTargets.getShade(shadeId);
225                     if (shade != null) {
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();
233                                 }
234                             }
235                         }
236                     }
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));
240             }
241         } catch (HubProcessingException | NumberFormatException e) {
242             logger.warn("Unexpected error: {}", e.getMessage());
243             return;
244         } catch (HubMaintenanceException e) {
245             // exceptions are logged in HDPowerViewWebTargets
246             return;
247         }
248     }
249
250     private int getShadeId() throws NumberFormatException {
251         String str = getConfigAs(HDPowerViewShadeConfiguration.class).id;
252         if (str == null) {
253             throw new NumberFormatException("null input string");
254         }
255         return Integer.parseInt(str);
256     }
257
258     private void stopShade() {
259         try {
260             HDPowerViewHubHandler bridge;
261             if ((bridge = getBridgeHandler()) == null) {
262                 throw new HubProcessingException("Missing bridge handler");
263             }
264             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
265             if (webTargets == null) {
266                 throw new HubProcessingException("Web targets not initialized");
267             }
268             int shadeId = getShadeId();
269             webTargets.stopShade(shadeId);
270             requestRefreshShadePosition();
271         } catch (HubProcessingException | NumberFormatException e) {
272             logger.warn("Unexpected error: {}", e.getMessage());
273             return;
274         } catch (HubMaintenanceException e) {
275             // exceptions are logged in HDPowerViewWebTargets
276             return;
277         }
278     }
279
280     /**
281      * Request that the shade shall undergo a 'hard' refresh for querying its current position
282      */
283     protected synchronized void requestRefreshShadePosition() {
284         if (refreshPositionFuture == null) {
285             refreshPositionFuture = scheduler.schedule(this::doRefreshShadePosition, REFRESH_DELAY_SEC,
286                     TimeUnit.SECONDS);
287         }
288     }
289
290     /**
291      * Request that the shade shall undergo a 'hard' refresh for querying its battery level state
292      */
293     protected synchronized void requestRefreshShadeBatteryLevel() {
294         if (refreshBatteryLevelFuture == null) {
295             refreshBatteryLevelFuture = scheduler.schedule(this::doRefreshShadeBatteryLevel, REFRESH_DELAY_SEC,
296                     TimeUnit.SECONDS);
297         }
298     }
299
300     private void doRefreshShadePosition() {
301         this.doRefreshShade(RefreshKind.POSITION);
302         refreshPositionFuture = null;
303     }
304
305     private void doRefreshShadeBatteryLevel() {
306         this.doRefreshShade(RefreshKind.BATTERY_LEVEL);
307         refreshBatteryLevelFuture = null;
308     }
309
310     private void doRefreshShade(RefreshKind kind) {
311         try {
312             HDPowerViewHubHandler bridge;
313             if ((bridge = getBridgeHandler()) == null) {
314                 throw new HubProcessingException("Missing bridge handler");
315             }
316             HDPowerViewWebTargets webTargets = bridge.getWebTargets();
317             if (webTargets == null) {
318                 throw new HubProcessingException("Web targets not initialized");
319             }
320             int shadeId = getShadeId();
321             Shade shade;
322             switch (kind) {
323                 case POSITION:
324                     shade = webTargets.refreshShadePosition(shadeId);
325                     break;
326                 case BATTERY_LEVEL:
327                     shade = webTargets.refreshShadeBatteryLevel(shadeId);
328                     break;
329                 default:
330                     throw new NotSupportedException("Unsupported refresh kind " + kind.toString());
331             }
332             if (shade != null) {
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);
337                     }
338                 }
339             }
340         } catch (HubProcessingException | NumberFormatException e) {
341             logger.warn("Unexpected error: {}", e.getMessage());
342         } catch (HubMaintenanceException e) {
343             // exceptions are logged in HDPowerViewWebTargets
344         }
345     }
346 }