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