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