]> git.basschouten.com Git - openhab-addons.git/blob
25c13a5d1e7e6d7a3cf7b059d219919ee487a159
[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 START_INTENT_CHANNEL:
184                 if (command instanceof RefreshType) {
185                     return;
186                 }
187                 adbConnection.startIntent(command.toFullString());
188                 break;
189             case RECORD_INPUT_CHANNEL:
190                 recordDeviceInput(command);
191                 break;
192             case RECORDED_INPUT_CHANNEL:
193                 String recordName = getRecordPropertyName(command);
194                 var inputCommand = this.getThing().getProperties().get(recordName);
195                 if (inputCommand != null) {
196                     adbConnection.sendInputEvents(inputCommand);
197                 }
198                 break;
199         }
200     }
201
202     private void recordDeviceInput(Command recordNameCommand)
203             throws AndroidDebugBridgeDeviceException, InterruptedException, TimeoutException, ExecutionException {
204         var recordName = recordNameCommand.toFullString();
205         if (!RECORD_NAME_PATTERN.matcher(recordName).matches()) {
206             logger.warn("Invalid record name, accepts alphanumeric values with '_'.");
207             return;
208         }
209         String recordPropertyName = getRecordPropertyName(recordName);
210         logger.debug("RECORD: {}", recordPropertyName);
211         var eventCommand = adbConnection.recordInputEvents();
212         if (eventCommand.isEmpty()) {
213             logger.debug("No events recorded");
214             if (this.getThing().getProperties().containsKey(recordPropertyName)) {
215                 this.getThing().setProperty(recordPropertyName, null);
216                 updateProperties(editProperties());
217                 logger.debug("Record {} deleted", recordName);
218             }
219         } else {
220             updateProperty(recordPropertyName, eventCommand);
221             logger.debug("New record {}: {}", recordName, eventCommand);
222         }
223     }
224
225     private String getRecordPropertyName(String recordName) {
226         return String.format("input-record:%s", recordName);
227     }
228
229     private String getRecordPropertyName(Command recordNameCommand) {
230         return getRecordPropertyName(recordNameCommand.toFullString());
231     }
232
233     private void handleMediaVolume(ChannelUID channelUID, Command command)
234             throws InterruptedException, AndroidDebugBridgeDeviceReadException, AndroidDebugBridgeDeviceException,
235             TimeoutException, ExecutionException {
236         if (command instanceof RefreshType) {
237             var volumeInfo = adbConnection.getMediaVolume();
238             maxMediaVolume = volumeInfo.max;
239             updateState(channelUID, new PercentType((int) Math.round(toPercent(volumeInfo.current, volumeInfo.max))));
240         } else {
241             if (maxMediaVolume == 0) {
242                 return; // We can not transform percentage
243             }
244             int targetVolume = Integer.parseInt(command.toFullString());
245             adbConnection.setMediaVolume((int) Math.round(fromPercent(targetVolume, maxMediaVolume)));
246             updateState(channelUID, new PercentType(targetVolume));
247         }
248     }
249
250     private double toPercent(double value, double maxValue) {
251         return (value / maxValue) * 100;
252     }
253
254     private double fromPercent(double value, double maxValue) {
255         return (value / 100) * maxValue;
256     }
257
258     private void handleMediaControlCommand(ChannelUID channelUID, Command command)
259             throws InterruptedException, AndroidDebugBridgeDeviceException, AndroidDebugBridgeDeviceReadException,
260             TimeoutException, ExecutionException {
261         if (command instanceof RefreshType) {
262             boolean playing;
263             String currentPackage = adbConnection.getCurrentPackage();
264             var currentPackageConfig = packageConfigs != null ? Arrays.stream(packageConfigs)
265                     .filter(pc -> pc.name.equals(currentPackage)).findFirst().orElse(null) : null;
266             if (currentPackageConfig != null) {
267                 logger.debug("media stream config found for {}, mode: {}", currentPackage, currentPackageConfig.mode);
268                 switch (currentPackageConfig.mode) {
269                     case "idle":
270                         playing = false;
271                         break;
272                     case "wake_lock":
273                         int wakeLockState = adbConnection.getPowerWakeLock();
274                         playing = currentPackageConfig.wakeLockPlayStates.contains(wakeLockState);
275                         break;
276                     case "media_state":
277                         playing = adbConnection.isPlayingMedia(currentPackage);
278                         break;
279                     case "audio":
280                         playing = adbConnection.isPlayingAudio();
281                         break;
282                     default:
283                         logger.warn("media state config: package {} unsupported mode", currentPackage);
284                         playing = false;
285                 }
286             } else {
287                 logger.debug("media stream config not found for {}", currentPackage);
288                 playing = adbConnection.isPlayingMedia(currentPackage);
289             }
290             updateState(channelUID, playing ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
291         } else if (command instanceof PlayPauseType) {
292             if (command == PlayPauseType.PLAY) {
293                 adbConnection.sendKeyEvent(KEY_EVENT_PLAY);
294                 updateState(channelUID, PlayPauseType.PLAY);
295             } else if (command == PlayPauseType.PAUSE) {
296                 adbConnection.sendKeyEvent(KEY_EVENT_PAUSE);
297                 updateState(channelUID, PlayPauseType.PAUSE);
298             }
299         } else if (command instanceof NextPreviousType) {
300             if (command == NextPreviousType.NEXT) {
301                 adbConnection.sendKeyEvent(KEY_EVENT_NEXT);
302             } else if (command == NextPreviousType.PREVIOUS) {
303                 adbConnection.sendKeyEvent(KEY_EVENT_PREVIOUS);
304             }
305         } else if (command instanceof RewindFastforwardType) {
306             if (command == RewindFastforwardType.FASTFORWARD) {
307                 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_FAST_FORWARD);
308             } else if (command == RewindFastforwardType.REWIND) {
309                 adbConnection.sendKeyEvent(KEY_EVENT_MEDIA_REWIND);
310             }
311         } else {
312             logger.warn("Unknown media control command: {}", command);
313         }
314     }
315
316     @Override
317     public void initialize() {
318         AndroidDebugBridgeConfiguration currentConfig = getConfigAs(AndroidDebugBridgeConfiguration.class);
319         config = currentConfig;
320         var mediaStateJSONConfig = currentConfig.mediaStateJSONConfig;
321         if (mediaStateJSONConfig != null && !mediaStateJSONConfig.isEmpty()) {
322             loadMediaStateConfig(mediaStateJSONConfig);
323         }
324         adbConnection.configure(currentConfig.ip, currentConfig.port, currentConfig.timeout,
325                 currentConfig.recordDuration);
326         updateStatus(ThingStatus.UNKNOWN);
327         connectionCheckerSchedule = scheduler.scheduleWithFixedDelay(this::checkConnection, 0,
328                 currentConfig.refreshTime, TimeUnit.SECONDS);
329     }
330
331     private void loadMediaStateConfig(String mediaStateJSONConfig) {
332         List<CommandOption> commandOptions;
333         try {
334             packageConfigs = GSON.fromJson(mediaStateJSONConfig, AndroidDebugBridgeMediaStatePackageConfig[].class);
335             commandOptions = Arrays.stream(packageConfigs)
336                     .map(AndroidDebugBridgeMediaStatePackageConfig::toCommandOption)
337                     .collect(Collectors.toUnmodifiableList());
338         } catch (JsonSyntaxException e) {
339             logger.warn("unable to parse media state config: {}", e.getMessage());
340             commandOptions = List.of();
341         }
342         commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), START_PACKAGE_CHANNEL),
343                 commandOptions);
344     }
345
346     @Override
347     public void dispose() {
348         var schedule = connectionCheckerSchedule;
349         if (schedule != null) {
350             schedule.cancel(true);
351             connectionCheckerSchedule = null;
352         }
353         packageConfigs = null;
354         adbConnection.disconnect();
355         super.dispose();
356     }
357
358     public void checkConnection() {
359         AndroidDebugBridgeConfiguration currentConfig = config;
360         try {
361             logger.debug("Refresh device {} status", currentConfig.ip);
362             if (adbConnection.isConnected()) {
363                 updateStatus(ThingStatus.ONLINE);
364                 refreshProperties();
365                 refreshStatus();
366             } else {
367                 try {
368                     adbConnection.connect();
369                 } catch (AndroidDebugBridgeDeviceException e) {
370                     logger.debug("Error connecting to device; [{}]: {}", e.getClass().getCanonicalName(),
371                             e.getMessage());
372                     adbConnection.disconnect();
373                     updateStatus(ThingStatus.OFFLINE);
374                     return;
375                 }
376                 if (adbConnection.isConnected()) {
377                     updateStatus(ThingStatus.ONLINE);
378                     refreshProperties();
379                     refreshStatus();
380                 }
381             }
382         } catch (InterruptedException ignored) {
383         } catch (AndroidDebugBridgeDeviceException | AndroidDebugBridgeDeviceReadException | ExecutionException e) {
384             logger.debug("Connection checker error: {}", e.getMessage());
385             adbConnection.disconnect();
386             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
387         }
388     }
389
390     private void refreshProperties() throws InterruptedException, AndroidDebugBridgeDeviceException,
391             AndroidDebugBridgeDeviceReadException, ExecutionException {
392         // Add some information about the device
393         try {
394             Map<String, String> editProperties = editProperties();
395             editProperties.put(Thing.PROPERTY_SERIAL_NUMBER, adbConnection.getSerialNo());
396             editProperties.put(Thing.PROPERTY_MODEL_ID, adbConnection.getModel());
397             editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, adbConnection.getAndroidVersion());
398             editProperties.put(Thing.PROPERTY_VENDOR, adbConnection.getBrand());
399             try {
400                 editProperties.put(Thing.PROPERTY_MAC_ADDRESS, adbConnection.getMacAddress());
401             } catch (AndroidDebugBridgeDeviceReadException e) {
402                 logger.debug("Refresh properties error: {}", e.getMessage());
403             }
404             updateProperties(editProperties);
405         } catch (TimeoutException e) {
406             logger.debug("Refresh properties error: Timeout");
407             return;
408         }
409     }
410
411     private void refreshStatus() throws InterruptedException, AndroidDebugBridgeDeviceException, ExecutionException {
412         boolean awakeState;
413         boolean prevDeviceAwake = deviceAwake;
414         try {
415             awakeState = adbConnection.isAwake();
416             deviceAwake = awakeState;
417         } catch (TimeoutException e) {
418             // happen a lot when device is sleeping; abort refresh other channels
419             logger.debug("Unable to refresh awake state: Timeout; aborting channels refresh");
420             return;
421         }
422         var awakeStateChannelUID = new ChannelUID(this.thing.getUID(), AWAKE_STATE_CHANNEL);
423         if (isLinked(awakeStateChannelUID)) {
424             updateState(awakeStateChannelUID, OnOffType.from(awakeState));
425         }
426         if (!awakeState && !prevDeviceAwake) {
427             // abort refresh channels while device is sleeping, throws many timeouts
428             logger.debug("device {} is sleeping", config.ip);
429             return;
430         }
431         try {
432             handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_VOLUME_CHANNEL), RefreshType.REFRESH);
433         } catch (AndroidDebugBridgeDeviceReadException e) {
434             logger.warn("Unable to refresh media volume: {}", e.getMessage());
435         } catch (TimeoutException e) {
436             logger.warn("Unable to refresh media volume: Timeout");
437         }
438         try {
439             handleCommandInternal(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL), RefreshType.REFRESH);
440         } catch (AndroidDebugBridgeDeviceReadException e) {
441             logger.warn("Unable to refresh play status: {}", e.getMessage());
442         } catch (TimeoutException e) {
443             logger.warn("Unable to refresh play status: Timeout");
444         }
445         try {
446             handleCommandInternal(new ChannelUID(this.thing.getUID(), CURRENT_PACKAGE_CHANNEL), RefreshType.REFRESH);
447         } catch (AndroidDebugBridgeDeviceReadException e) {
448             logger.warn("Unable to refresh current package: {}", e.getMessage());
449         } catch (TimeoutException e) {
450             logger.warn("Unable to refresh current package: Timeout");
451         }
452         try {
453             handleCommandInternal(new ChannelUID(this.thing.getUID(), WAKE_LOCK_CHANNEL), RefreshType.REFRESH);
454         } catch (AndroidDebugBridgeDeviceReadException e) {
455             logger.warn("Unable to refresh wake lock: {}", e.getMessage());
456         } catch (TimeoutException e) {
457             logger.warn("Unable to refresh wake lock: Timeout");
458         }
459         try {
460             handleCommandInternal(new ChannelUID(this.thing.getUID(), SCREEN_STATE_CHANNEL), RefreshType.REFRESH);
461         } catch (AndroidDebugBridgeDeviceReadException e) {
462             logger.warn("Unable to refresh screen state: {}", e.getMessage());
463         } catch (TimeoutException e) {
464             logger.warn("Unable to refresh screen state: Timeout");
465         }
466     }
467
468     static class AndroidDebugBridgeMediaStatePackageConfig {
469         public String name = "";
470         public @Nullable String label;
471         public String mode = "";
472         public List<Integer> wakeLockPlayStates = List.of();
473
474         public CommandOption toCommandOption() {
475             return new CommandOption(name, label == null ? name : label);
476         }
477     }
478 }