]> git.basschouten.com Git - openhab-addons.git/blob
b92f56129ca0b92d2b2affc1d4e1b0d4dec71299
[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.linuxinput.internal;
14
15 import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.THING_TYPE_DEVICE;
16
17 import java.io.File;
18 import java.io.IOException;
19 import java.nio.file.*;
20 import java.time.Duration;
21 import java.util.Collections;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.FutureTask;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.linuxinput.internal.evdev4j.EvdevDevice;
29 import org.openhab.binding.linuxinput.internal.evdev4j.LastErrorException;
30 import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
31 import org.openhab.core.config.discovery.AbstractDiscoveryService;
32 import org.openhab.core.config.discovery.DiscoveryResult;
33 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
34 import org.openhab.core.config.discovery.DiscoveryService;
35 import org.openhab.core.thing.ThingUID;
36 import org.osgi.service.component.annotations.Component;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * Discovery service for LinuxInputHandlers based on the /dev/input directory.
42  *
43  * @author Thomas Weißschuh - Initial contribution
44  */
45 @Component(service = DiscoveryService.class, configurationPid = "discovery.linuxinput")
46 @NonNullByDefault
47 public class LinuxInputDiscoveryService extends AbstractDiscoveryService {
48
49     private static final Duration REFRESH_INTERVAL = Duration.ofSeconds(50);
50     private static final Duration TIMEOUT = Duration.ofSeconds(30);
51     private static final Duration EVENT_TIMEOUT = Duration.ofSeconds(60);
52     private static final Path DEVICE_DIRECTORY = FileSystems.getDefault().getPath("/dev/input");
53
54     private final Logger logger = LoggerFactory.getLogger(LinuxInputDiscoveryService.class);
55     private @NonNullByDefault({}) Future<?> discoveryJob;
56
57     public LinuxInputDiscoveryService() {
58         super(Collections.singleton(THING_TYPE_DEVICE), (int) TIMEOUT.getSeconds(), true);
59     }
60
61     @Override
62     protected void startScan() {
63         performScan(false);
64     }
65
66     private void performScan(boolean applyTtl) {
67         logger.debug("Scanning directory {} for devices", DEVICE_DIRECTORY);
68         removeOlderResults(getTimestampOfLastScan());
69         File directory = DEVICE_DIRECTORY.toFile();
70         Duration ttl = null;
71         if (applyTtl) {
72             ttl = REFRESH_INTERVAL.multipliedBy(2);
73         }
74         if (directory == null) {
75             logger.warn("Could not open device directory {}", DEVICE_DIRECTORY);
76             return;
77         }
78         File[] devices = directory.listFiles();
79         if (devices == null) {
80             logger.warn("Could not enumerate {}, it is not a directory", directory);
81             return;
82         }
83         for (File file : devices) {
84             handleFile(file, ttl);
85         }
86     }
87
88     private void handleFile(File file, @Nullable Duration ttl) {
89         logger.trace("Discovering file {}", file);
90         if (file.isDirectory()) {
91             logger.trace("{} is not a file, ignoring", file);
92             return;
93         }
94         if (!file.canRead()) {
95             logger.debug("{} is not readable, ignoring", file);
96             return;
97         }
98         DiscoveryResultBuilder result = DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_DEVICE, file.getName()))
99                 .withProperty("path", file.getAbsolutePath()).withRepresentationProperty(file.getName());
100         if (ttl != null) {
101             result = result.withTTL(ttl.getSeconds());
102         }
103
104         boolean shouldDiscover = enrichDevice(result, file);
105         if (shouldDiscover) {
106             DiscoveryResult thing = result.build();
107             logger.debug("Discovered: {}", thing);
108             thingDiscovered(thing);
109         } else {
110             logger.debug("{} is not a keyboard, ignoring", file);
111         }
112     }
113
114     private boolean enrichDevice(DiscoveryResultBuilder builder, File inputDevice) {
115         String label = inputDevice.getName();
116         try {
117             try (EvdevDevice device = new EvdevDevice(inputDevice.getAbsolutePath())) {
118                 String labelFromDevice = device.getName();
119                 boolean isKeyboard = device.has(EvdevLibrary.Type.KEY);
120                 if (labelFromDevice != null) {
121                     label = labelFromDevice;
122                 }
123                 return isKeyboard;
124             } finally {
125                 builder.withLabel(label);
126             }
127         } catch (IOException | LastErrorException e) {
128             logger.debug("Could not open device {}", inputDevice, e);
129             return false;
130         }
131     }
132
133     @Override
134     protected synchronized void stopScan() {
135         super.stopScan();
136         removeOlderResults(getTimestampOfLastScan());
137     }
138
139     @Override
140     protected void startBackgroundDiscovery() {
141         logger.debug("Starting background discovery");
142         if (discoveryJob == null || discoveryJob.isCancelled()) {
143             WatchService watchService = null;
144             try {
145                 watchService = makeWatcher();
146             } catch (IOException e) {
147                 logger.debug("Could not start event based watcher, falling back to polling", e);
148             }
149             if (watchService != null) {
150                 WatchService watcher = watchService;
151                 FutureTask<?> job = new FutureTask<>(() -> {
152                     waitForNewDevices(watcher);
153                     return null;
154                 });
155                 Thread t = Utils.backgroundThread(job, "discovery", null);
156                 t.start();
157                 discoveryJob = job;
158             } else {
159                 discoveryJob = scheduler.scheduleWithFixedDelay(() -> performScan(true), 0,
160                         REFRESH_INTERVAL.getSeconds(), TimeUnit.SECONDS);
161             }
162         }
163     }
164
165     private WatchService makeWatcher() throws IOException {
166         WatchService watchService = FileSystems.getDefault().newWatchService();
167         DEVICE_DIRECTORY.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
168                 StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
169         return watchService;
170     }
171
172     private void waitForNewDevices(WatchService watchService) {
173         while (!Thread.currentThread().isInterrupted()) {
174             boolean gotEvent = waitAndDrainAll(watchService);
175             logger.debug("Input devices changed: {}. Triggering rescan: {}", gotEvent, gotEvent);
176
177             if (gotEvent) {
178                 performScan(false);
179             }
180         }
181         logger.debug("Discovery stopped");
182     }
183
184     private static boolean waitAndDrainAll(WatchService watchService) {
185         WatchKey event;
186         try {
187             event = watchService.poll(EVENT_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
188         } catch (InterruptedException e) {
189             Thread.currentThread().interrupt();
190             return false;
191         }
192         if (event == null) {
193             return false;
194         }
195         do {
196             event.pollEvents();
197             event.reset();
198             event = watchService.poll();
199         } while (event != null);
200
201         return true;
202     }
203
204     @Override
205     protected void stopBackgroundDiscovery() {
206         logger.debug("Stopping background discovery");
207         if (discoveryJob != null && !discoveryJob.isCancelled()) {
208             discoveryJob.cancel(true);
209             discoveryJob = null;
210         }
211     }
212 }