2 * Copyright (c) 2010-2021 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.*;
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;
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;
41 * Discovery service for LinuxInputHandlers based on the /dev/input directory.
43 * @author Thomas Weißschuh - Initial contribution
45 @Component(service = DiscoveryService.class, configurationPid = "discovery.linuxinput")
47 public class LinuxInputDiscoveryService extends AbstractDiscoveryService {
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");
54 private final Logger logger = LoggerFactory.getLogger(LinuxInputDiscoveryService.class);
55 private @NonNullByDefault({}) Future<?> discoveryJob;
57 public LinuxInputDiscoveryService() {
58 super(Collections.singleton(THING_TYPE_DEVICE), (int) TIMEOUT.getSeconds(), true);
62 protected void startScan() {
66 private void performScan(boolean applyTtl) {
67 logger.debug("Scanning directory {} for devices", DEVICE_DIRECTORY);
68 removeOlderResults(getTimestampOfLastScan());
69 File directory = DEVICE_DIRECTORY.toFile();
72 ttl = REFRESH_INTERVAL.multipliedBy(2);
74 if (directory == null) {
75 logger.warn("Could not open device directory {}", DEVICE_DIRECTORY);
78 File[] devices = directory.listFiles();
79 if (devices == null) {
80 logger.warn("Could not enumerate {}, it is not a directory", directory);
83 for (File file : devices) {
84 handleFile(file, ttl);
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);
94 if (!file.canRead()) {
95 logger.debug("{} is not readable, ignoring", file);
98 DiscoveryResultBuilder result = DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_DEVICE, file.getName()))
99 .withProperty("path", file.getAbsolutePath()).withRepresentationProperty(file.getName());
101 result = result.withTTL(ttl.getSeconds());
104 boolean shouldDiscover = enrichDevice(result, file);
105 if (shouldDiscover) {
106 DiscoveryResult thing = result.build();
107 logger.debug("Discovered: {}", thing);
108 thingDiscovered(thing);
110 logger.debug("{} is not a keyboard, ignoring", file);
114 private boolean enrichDevice(DiscoveryResultBuilder builder, File inputDevice) {
115 String label = inputDevice.getName();
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;
125 builder.withLabel(label);
127 } catch (IOException | LastErrorException e) {
128 logger.debug("Could not open device {}", inputDevice, e);
134 protected synchronized void stopScan() {
136 removeOlderResults(getTimestampOfLastScan());
140 protected void startBackgroundDiscovery() {
141 logger.debug("Starting background discovery");
142 if (discoveryJob == null || discoveryJob.isCancelled()) {
143 WatchService watchService = null;
145 watchService = makeWatcher();
146 } catch (IOException e) {
147 logger.debug("Could not start event based watcher, falling back to polling", e);
149 if (watchService != null) {
150 WatchService watcher = watchService;
151 FutureTask<?> job = new FutureTask<>(() -> {
152 waitForNewDevices(watcher);
155 Thread t = Utils.backgroundThread(job, getClass(), null);
159 discoveryJob = scheduler.scheduleWithFixedDelay(() -> performScan(true), 0,
160 REFRESH_INTERVAL.getSeconds(), TimeUnit.SECONDS);
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);
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);
181 logger.debug("Discovery stopped");
184 private static boolean waitAndDrainAll(WatchService watchService) {
187 event = watchService.poll(EVENT_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
188 } catch (InterruptedException e) {
189 Thread.currentThread().interrupt();
198 event = watchService.poll();
199 } while (event != null);
205 protected void stopBackgroundDiscovery() {
206 logger.debug("Stopping background discovery");
207 if (discoveryJob != null && !discoveryJob.isCancelled()) {
208 discoveryJob.cancel(true);