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