]> git.basschouten.com Git - openhab-addons.git/blob
39d18c33295f8f8ee8b435ef60ed1fb7ad7af042
[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.androiddebugbridge.internal;
14
15 import static org.openhab.binding.androiddebugbridge.internal.AndroidDebugBridgeBindingConstants.*;
16
17 import java.util.Arrays;
18 import java.util.List;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.NextPreviousType;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.PercentType;
30 import org.openhab.core.library.types.PlayPauseType;
31 import org.openhab.core.library.types.RewindFastforwardType;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.BaseThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import com.google.gson.Gson;
44 import com.google.gson.JsonSyntaxException;
45
46 /**
47  * The {@link AndroidDebugBridgeHandler} is responsible for handling commands, which are
48  * sent to one of the channels.
49  *
50  * @author Miguel Álvarez - Initial contribution
51  */
52 @NonNullByDefault
53 public class AndroidDebugBridgeHandler extends BaseThingHandler {
54
55     public static final String KEY_EVENT_PLAY = "126";
56     public static final String KEY_EVENT_PAUSE = "127";
57     public static final String KEY_EVENT_NEXT = "87";
58     public static final String KEY_EVENT_PREVIOUS = "88";
59     public static final String KEY_EVENT_MEDIA_REWIND = "89";
60     public static final String KEY_EVENT_MEDIA_FAST_FORWARD = "90";
61     private static final String SHUTDOWN_POWER_OFF = "POWER_OFF";
62     private static final String SHUTDOWN_REBOOT = "REBOOT";
63     private static final Gson GSON = new Gson();
64     private final Logger logger = LoggerFactory.getLogger(AndroidDebugBridgeHandler.class);
65     private final AndroidDebugBridgeDevice adbConnection;
66     private int maxMediaVolume = 0;
67     private AndroidDebugBridgeConfiguration config = new AndroidDebugBridgeConfiguration();
68     private @Nullable ScheduledFuture<?> connectionCheckerSchedule;
69     private AndroidDebugBridgeMediaStatePackageConfig @Nullable [] packageConfigs = null;
70     private boolean deviceAwake = false;
71
72     public AndroidDebugBridgeHandler(Thing thing) {
73         super(thing);
74         this.adbConnection = new AndroidDebugBridgeDevice(scheduler);
75     }
76
77     @Override
78     public void handleCommand(ChannelUID channelUID, Command command) {
79         var currentConfig = config;
80         if (currentConfig == null) {
81             return;
82         }
83         try {
84             if (!adbConnection.isConnected()) {
85                 // try reconnect
86                 adbConnection.connect();
87             }
88             handleCommandInternal(channelUID, command);
89         } catch (InterruptedException ignored) {
90         } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
91             if (!(e.getCause() instanceof InterruptedException)) {
92                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
93                 adbConnection.disconnect();
94             }
95         } catch (AndroidDebugBridgeDeviceReadException e) {
96             logger.warn("{} - read error: {}", currentConfig.ip, e.getMessage());
97         } catch (TimeoutException e) {
98             logger.warn("{} - timeout error", currentConfig.ip);
99         }
100     }
101
102     private void handleCommandInternal(ChannelUID channelUID, Command command)
103             throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
104             TimeoutException, ExecutionException {
105         if (!isLinked(channelUID)) {
106             return;
107         }
108         String channelId = channelUID.getId();
109         switch (channelId) {
110             case KEY_EVENT_CHANNEL:
111                 adbConnection.sendKeyEvent(command.toFullString());
112                 break;
113             case TEXT_CHANNEL:
114                 adbConnection.sendText(command.toFullString());
115                 break;
116             case TAP_CHANNEL:
117                 adbConnection.sendTap(command.toFullString());
118                 break;
119             case MEDIA_VOLUME_CHANNEL:
120                 handleMediaVolume(channelUID, command);
121                 break;
122             case MEDIA_CONTROL_CHANNEL:
123                 handleMediaControlCommand(channelUID, command);
124                 break;
125             case START_PACKAGE_CHANNEL:
126                 adbConnection.startPackage(command.toFullString());
127                 updateState(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL),
128                         new StringType(command.toFullString()));
129                 break;
130             case STOP_PACKAGE_CHANNEL:
131                 adbConnection.stopPackage(command.toFullString());
132                 break;
133             case STOP_CURRENT_PACKAGE_CHANNEL:
134                 if (OnOffType.from(command.toFullString()).equals(OnOffType.OFF)) {
135                     adbConnection.stopPackage(adbConnection.getCurrentPackage());
136                 }
137                 break;
138             case CURRENT_PACKAGE_CHANNEL:
139                 if (command instanceof RefreshType) {
140                     var packageName = adbConnection.getCurrentPackage();
141                     updateState(channelUID, new StringType(packageName));
142                 }
143                 break;
144             case WAKE_LOCK_CHANNEL:
145                 if (command instanceof RefreshType) {
146                     int lock = adbConnection.getPowerWakeLock();
147                     updateState(channelUID, new DecimalType(lock));
148                 }
149                 break;
150             case AWAKE_STATE_CHANNEL:
151                 if (command instanceof RefreshType) {
152                     boolean awakeState = adbConnection.isAwake();
153                     updateState(channelUID, OnOffType.from(awakeState));
154                 }
155                 break;
156             case SCREEN_STATE_CHANNEL:
157                 if (command instanceof RefreshType) {
158                     boolean screenState = adbConnection.isScreenOn();
159                     updateState(channelUID, OnOffType.from(screenState));
160                 }
161                 break;
162             case SHUTDOWN_CHANNEL:
163                 switch (command.toFullString()) {
164                     case SHUTDOWN_POWER_OFF:
165                         adbConnection.powerOffDevice();
166                         updateStatus(ThingStatus.OFFLINE);
167                         break;
168                     case SHUTDOWN_REBOOT:
169                         adbConnection.rebootDevice();
170                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Rebooting");
171                         break;
172                 }
173         }
174     }
175
176     private void handleMediaVolume(ChannelUID channelUID, Command command)
177             throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
178             TimeoutException, ExecutionException {
179         if (command instanceof RefreshType) {
180             var volumeInfo = adbConnection.getMediaVolume();
181             maxMediaVolume = volumeInfo.max;
182             updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
183         } else {
184             if (maxMediaVolume == 0) {
185                 return; // We can not transform percentage
186             }
187             int targetVolume = Integer.parseInt(command.toFullString());
188             adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
189             updateState(channelUID, new PercentType(targetVolume));
190         }
191     }
192
193     private double toPercent(double value, double maxValue) {
194         return (value / maxValue) * 100;
195     }
196
197     private double fromPercent(double value, double maxValue) {
198         return (value / 100) * maxValue;
199     }
200
201     private void handleMediaControlCommand(ChannelUID channelUID, Command command)
202             throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
203             TimeoutException, ExecutionException {
204         if (command instanceof RefreshType) {
205             boolean playing;
206             String currentPackage = adbConnection.getCurrentPackage();
207             var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
208                     .filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
209             if (currentPackageConfig != null) {
210                 logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
211                 switch (currentPackageConfig.mode) {
212                     case "idle":
213                         playing = false;
214                         break;
215                     case "wake_lock":
216                         int wakeLockState = adbConnection.getPowerWakeLock();
217                         playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
218                         break;
219                     case "media_state":
220                         playing = adbConnection.isPlayingMedia(currentPackage);
221                         break;
222                     case "audio":
223                         playing = adbConnection.isPlayingAudio();
224                         break;
225                     default:
226                         logger.warn("media state config: package {} unsupported mode", currentPackage);
227                         playing = false;
228                 }
229             } else {
230                 logger.debug("media stream config not found for {}", currentPackage);
231                 playing = adbConnection.isPlayingMedia(currentPackage);
232             }
233             updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
234         } else if (command instanceof PlayPauseType) {
235             if (command == PlayPauseType.PLAY) {
236                 adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
237                 updateState(channelUID, PlayPauseType.PLAY);
238             } else if (command == PlayPauseType.PAUSE) {
239                 adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
240                 updateState(channelUID, PlayPauseType.PAUSE);
241             }
242         } else if (command instanceof NextPreviousType) {
243             if (command == NextPreviousType.NEXT) {
244                 adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
245             } else if (command == NextPreviousType.PREVIOUS) {
246                 adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
247             }
248         } else if (command instanceof RewindFastforwardType) {
249             if (command == RewindFastforwardType.FASTFORWARD) {
250                 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
251             } else if (command == RewindFastforwardType.REWIND) {
252                 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
253             }
254         } else {
255             logger.warn("Unknown media control command: {}", command);
256         }
257     }
258
259     @Override
260     public void initialize() {
261         var currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
262         config = currentConfig;
263         var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
264         if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
265             loadMediaStateConfig(mediaStateJSONConfig);
266         }
267         adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout);
268         updateStatus(ThingStatus.UNKNOWN);
269         connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
270                 currentConfig.refreshTime, TimeUnit.SECONDS);
271     }
272
273     private void loadMediaStateConfig(String mediaStateJSONConfig) {
274         try {
275             this.packageConfigs = GSON.fromJson(mediaStateJSONConfig,
276                     AndroidDebugBridgeMediaStatePackageConfig[].class);
277         } catch (JsonSyntaxException e) {
278             logger.warn("unable to parse media state config: {}", e.getMessage());
279         }
280     }
281
282     @Override
283     public void dispose() {
284         var schedule = connectionCheckerSchedule;
285         if (schedule != null) {
286             schedule.cancel(true);
287             connectionCheckerSchedule = null;
288         }
289         packageConfigs = null;
290         adbConnection.disconnect();
291         super.dispose();
292     }
293
294     public void checkConnection() {
295         var currentConfig = config;
296         if (currentConfig == null) {
297             return;
298         }
299         try {
300             logger.debug("Refresh device {} status", currentConfig.ip);
301             if (adbConnection.isConnected()) {
302                 updateStatus(ThingStatus.ONLINE);
303                 refreshStatus();
304             } else {
305                 try {
306                     adbConnection.connect();
307                 } catch (AndroidDebugBridgeDeviceException e) {
308                     logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
309                             e.getMessage());
310                     adbConnection.disconnect();
311                     updateStatus(ThingStatus.OFFLINE);
312                     return;
313                 }
314                 if (adbConnection.isConnected()) {
315                     updateStatus(ThingStatus.ONLINE);
316                     refreshStatus();
317                 }
318             }
319         } catch (InterruptedException ignored) {
320         } catch (AndroidDebugBridgeDeviceException | ExecutionException e) {
321             logger.debug("Connection checker error: {}", e.getMessage());
322             adbConnection.disconnect();
323             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
324         }
325     }
326
327     private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
328         boolean awakeState;
329         boolean prevDeviceAwake = deviceAwake;
330         try {
331             awakeState = adbConnection.isAwake();
332             deviceAwake = awakeState;
333         } catch (TimeoutException e) {
334             logger.warn("Unable to refresh awake state: Timeout");
335             return;
336         }
337         var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
338         if (isLinked(awakeStateChannelUID)) {
339             updateState(awakeStateChannelUID, OnOffType.from(awakeState));
340         }
341         if (!awakeState && !prevDeviceAwake) {
342             logger.debug("device {} is sleeping", config.ip);
343             return;
344         }
345         try {
346             handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
347         } catch (AndroidDebugBridgeDeviceReadException e) {
348             logger.warn("Unable to refresh media volume: {}", e.getMessage());
349         } catch (TimeoutException e) {
350             logger.warn("Unable to refresh media volume: Timeout");
351         }
352         try {
353             handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
354         } catch (AndroidDebugBridgeDeviceReadException e) {
355             logger.warn("Unable to refresh play status: {}", e.getMessage());
356         } catch (TimeoutException e) {
357             logger.warn("Unable to refresh play status: Timeout");
358         }
359         try {
360             handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
361         } catch (AndroidDebugBridgeDeviceReadException e) {
362             logger.warn("Unable to refresh current package: {}", e.getMessage());
363         } catch (TimeoutException e) {
364             logger.warn("Unable to refresh current package: Timeout");
365         }
366         try {
367             handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
368         } catch (AndroidDebugBridgeDeviceReadException e) {
369             logger.warn("Unable to refresh wake lock: {}", e.getMessage());
370         } catch (TimeoutException e) {
371             logger.warn("Unable to refresh wake lock: Timeout");
372         }
373         try {
374             handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
375         } catch (AndroidDebugBridgeDeviceReadException e) {
376             logger.warn("Unable to refresh screen state: {}", e.getMessage());
377         } catch (TimeoutException e) {
378             logger.warn("Unable to refresh screen state: Timeout");
379         }
380     }
381
382     static class AndroidDebugBridgeMediaStatePackageConfig {
383         public String name = "";
384         public String mode = "";
385         public List<Integer> wakeLockPlayStates = List.of();
386     }
387 }