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