2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.linuxinput.internal;
15 import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.THING_TYPE_DEVICE;
18 import java.io.IOException;
19 import java.nio.file.FileSystems;
20 import java.nio.file.Path;
21 import java.nio.file.StandardWatchEventKinds;
22 import java.nio.file.WatchKey;
23 import java.nio.file.WatchService;
24 import java.time.Duration;
25 import java.util.Collections;
26 import java.util.concurrent.Future;
27 import java.util.concurrent.FutureTask;
28 import java.util.concurrent.TimeUnit;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.linuxinput.internal.evdev4j.EvdevDevice;
33 import org.openhab.binding.linuxinput.internal.evdev4j.LastErrorException;
34 import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
35 import org.openhab.core.config.discovery.AbstractDiscoveryService;
36 import org.openhab.core.config.discovery.DiscoveryResult;
37 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
38 import org.openhab.core.config.discovery.DiscoveryService;
39 import org.openhab.core.thing.ThingUID;
40 import org.osgi.service.component.annotations.Component;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * Discovery service for LinuxInputHandlers based on the /dev/input directory.
47 * @author Thomas Weißschuh - Initial contribution
49 @Component(service = DiscoveryService.class, configurationPid = "discovery.linuxinput")
51 public class LinuxInputDiscoveryService extends AbstractDiscoveryService {
53 private static final Duration REFRESH_INTERVAL = Duration.ofSeconds(50);
54 private static final Duration TIMEOUT = Duration.ofSeconds(30);
55 private static final Duration EVENT_TIMEOUT = Duration.ofSeconds(60);
56 private static final Path DEVICE_DIRECTORY = FileSystems.getDefault().getPath("/dev/input");
58 private final Logger logger = LoggerFactory.getLogger(LinuxInputDiscoveryService.class);
59 private @NonNullByDefault({}) Future<?> discoveryJob;
61 public LinuxInputDiscoveryService() {
62 super(Collections.singleton(THING_TYPE_DEVICE), (int) TIMEOUT.getSeconds(), true);
66 protected void startScan() {
70 private void performScan(boolean applyTtl) {
71 logger.debug("Scanning directory {} for devices", DEVICE_DIRECTORY);
72 removeOlderResults(getTimestampOfLastScan());
73 File directory = DEVICE_DIRECTORY.toFile();
76 ttl = REFRESH_INTERVAL.multipliedBy(2);
78 if (directory == null) {
79 logger.warn("Could not open device directory {}", DEVICE_DIRECTORY);
82 File[] devices = directory.listFiles();
83 if (devices == null) {
84 logger.warn("Could not enumerate {}, it is not a directory", directory);
87 for (File file : devices) {
88 handleFile(file, ttl);
92 private void handleFile(File file, @Nullable Duration ttl) {
93 logger.trace("Discovering file {}", file);
94 if (file.isDirectory()) {
95 logger.trace("{} is not a file, ignoring", file);
98 if (!file.canRead()) {
99 logger.debug("{} is not readable, ignoring", file);
102 DiscoveryResultBuilder result = DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_DEVICE, file.getName()))
103 .withProperty("path", file.getAbsolutePath()).withRepresentationProperty("path");
105 result = result.withTTL(ttl.getSeconds());
108 boolean shouldDiscover = enrichDevice(result, file);
109 if (shouldDiscover) {
110 DiscoveryResult thing = result.build();
111 logger.debug("Discovered: {}", thing);
112 thingDiscovered(thing);
114 logger.debug("{} is not a keyboard, ignoring", file);
118 private boolean enrichDevice(DiscoveryResultBuilder builder, File inputDevice) {
119 String label = inputDevice.getName();
121 try (EvdevDevice device = new EvdevDevice(inputDevice.getAbsolutePath())) {
122 String labelFromDevice = device.getName();
123 boolean isKeyboard = device.has(EvdevLibrary.Type.KEY);
124 if (labelFromDevice != null) {
125 label = String.format("%s (%s)", labelFromDevice, inputDevice.getName());
129 builder.withLabel(label);
131 } catch (IOException | LastErrorException e) {
132 logger.debug("Could not open device {}", inputDevice, e);
138 protected synchronized void stopScan() {
140 removeOlderResults(getTimestampOfLastScan());
144 protected void startBackgroundDiscovery() {
145 logger.debug("Starting background discovery");
146 if (discoveryJob == null || discoveryJob.isCancelled()) {
147 WatchService watchService = null;
149 watchService = makeWatcher();
150 } catch (IOException e) {
151 logger.debug("Could not start event based watcher, falling back to polling", e);
153 if (watchService != null) {
154 WatchService watcher = watchService;
155 FutureTask<?> job = new FutureTask<>(() -> {
156 waitForNewDevices(watcher);
159 Thread t = Utils.backgroundThread(job, "discovery", null);
163 discoveryJob = scheduler.scheduleWithFixedDelay(() -> performScan(true), 0,
164 REFRESH_INTERVAL.getSeconds(), TimeUnit.SECONDS);
169 private WatchService makeWatcher() throws IOException {
170 WatchService watchService = FileSystems.getDefault().newWatchService();
171 // FIXME also trigger on inotify "ATTRIB" events when WatchService supports this.
172 // Triggering on ENTRY_MODIFY will trigger multiple times on each keypress for *any* input device.
173 DEVICE_DIRECTORY.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
174 StandardWatchEventKinds.ENTRY_DELETE);
178 private void waitForNewDevices(WatchService watchService) {
179 while (!Thread.currentThread().isInterrupted()) {
180 boolean gotEvent = waitAndDrainAll(watchService);
181 logger.debug("Input devices changed: {}. Triggering rescan: {}", gotEvent, gotEvent);
187 logger.debug("Discovery stopped");
190 private static boolean waitAndDrainAll(WatchService watchService) {
193 event = watchService.poll(EVENT_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
194 } catch (InterruptedException e) {
195 Thread.currentThread().interrupt();
204 event = watchService.poll();
205 } while (event != null);
211 protected void stopBackgroundDiscovery() {
212 logger.debug("Stopping background discovery");
213 if (discoveryJob != null && !discoveryJob.isCancelled()) {
214 discoveryJob.cancel(true);