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