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