added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.linuxinput-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-linuxinput" description="Linux Input Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature prerequisite="true">wrap</feature>
|
||||
<bundle dependency="true">mvn:com.github.jnr/jnr-enxio/0.19</bundle>
|
||||
<!-- we have to copy and extend the already provided imports -->
|
||||
<bundle dependency="true">wrap:mvn:com.github.jnr/jnr-posix/3.0.47$overwrite=merge&Import-Package=jnr.ffi.provider.jffi,jnr.constants,jnr.constants.platform,jnr.constants.platform.windows,jnr.ffi,jnr.ffi.annotations,jnr.ffi.byref,jnr.ffi.mapper,jnr.ffi.provider,jnr.ffi.types,com.kenai.jffi,jnr.ffi.provider.converters</bundle>
|
||||
<bundle>mvn:com.github.jnr/jffi/1.2.18/jar/native</bundle>
|
||||
<bundle dependency="true">mvn:com.github.jnr/jffi/1.2.18/jar/complete</bundle>
|
||||
<bundle dependency="true">mvn:com.github.jnr/jnr-constants/0.9.11</bundle>
|
||||
<bundle dependency="true">mvn:com.github.jnr/jnr-ffi/2.1.9</bundle>
|
||||
<bundle dependency="true">wrap:mvn:com.github.jnr/jnr-a64asm/1.0.0$Bundle-Name=jnr-a64asm&Bundle-SymbolicName=com.github.jnr.jnr-a64asm&Bundle-Version=1.0.0</bundle>
|
||||
<bundle dependency="true">wrap:mvn:com.github.jnr/jnr-x86asm/1.0.2$Bundle-Name=jnr-x86asm&Bundle-SymbolicName=com.github.jnr.jnr-x86asm&Bundle-Version=1.0.2</bundle>
|
||||
<bundle dependency="true">mvn:org.ow2.asm/asm/5.0.3</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.linuxinput/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Abstract handler, that encapsulates the lifecycle of an underlying device.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class DeviceReadingHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(DeviceReadingHandler.class);
|
||||
|
||||
private @Nullable Thread worker = null;
|
||||
|
||||
public DeviceReadingHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
abstract boolean immediateSetup() throws IOException;
|
||||
|
||||
abstract boolean delayedSetup() throws IOException;
|
||||
|
||||
abstract void handleEventsInThread() throws IOException;
|
||||
|
||||
abstract void closeDevice() throws IOException;
|
||||
|
||||
abstract String getInstanceName();
|
||||
|
||||
@Override
|
||||
public final void initialize() {
|
||||
boolean performDelayedSetup = performImmediateSetup();
|
||||
if (performDelayedSetup) {
|
||||
scheduler.execute(() -> {
|
||||
boolean handleEvents = performDelayedSetup();
|
||||
if (handleEvents) {
|
||||
Thread thread = Utils.backgroundThread(() -> {
|
||||
try {
|
||||
handleEventsInThread();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not read event", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}, getClass(), getInstanceName());
|
||||
thread.start();
|
||||
worker = thread;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean performImmediateSetup() {
|
||||
try {
|
||||
return immediateSetup();
|
||||
} catch (IOException e) {
|
||||
handleSetupError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean performDelayedSetup() {
|
||||
try {
|
||||
return delayedSetup();
|
||||
} catch (IOException e) {
|
||||
handleSetupError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSetupError(Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void dispose() {
|
||||
try {
|
||||
stopWorker();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
try {
|
||||
closeDevice();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Could not close device", e);
|
||||
}
|
||||
logger.trace("disposed");
|
||||
}
|
||||
}
|
||||
|
||||
private void stopWorker() throws InterruptedException {
|
||||
@Nullable
|
||||
Thread activeWorker = this.worker;
|
||||
logger.debug("interrupting worker {}", activeWorker);
|
||||
worker = null;
|
||||
|
||||
if (activeWorker == null) {
|
||||
return;
|
||||
}
|
||||
activeWorker.interrupt();
|
||||
try {
|
||||
activeWorker.join(100);
|
||||
} catch (CancellationException e) {
|
||||
/* expected */
|
||||
}
|
||||
logger.debug("worker interrupted");
|
||||
if (activeWorker.isAlive()) {
|
||||
logger.warn("Worker not stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* Constants shared by the LinuxInput binding components.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LinuxInputBindingConstants {
|
||||
private LinuxInputBindingConstants() {
|
||||
}
|
||||
|
||||
public static final String BINDING_ID = "linuxinput";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "input-device");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_KEY_PRESS = new ChannelTypeUID(BINDING_ID, "key-press");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_KEY = new ChannelTypeUID(BINDING_ID, "key");
|
||||
public static final String CHANNEL_GROUP_KEYPRESSES_ID = "keypresses";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
/**
|
||||
* Configuration for LinuxInput devices
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
public class LinuxInputConfiguration {
|
||||
|
||||
public String path;
|
||||
public boolean enable;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.THING_TYPE_DEVICE;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.EvdevDevice;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.LastErrorException;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Discovery service for LinuxInputHandlers based on the /dev/input directory.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.linuxinput")
|
||||
@NonNullByDefault
|
||||
public class LinuxInputDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private static final Duration REFRESH_INTERVAL = Duration.ofSeconds(50);
|
||||
private static final Duration TIMEOUT = Duration.ofSeconds(30);
|
||||
private static final Duration EVENT_TIMEOUT = Duration.ofSeconds(60);
|
||||
private static final Path DEVICE_DIRECTORY = FileSystems.getDefault().getPath("/dev/input");
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LinuxInputDiscoveryService.class);
|
||||
private @NonNullByDefault({}) Future<?> discoveryJob;
|
||||
|
||||
public LinuxInputDiscoveryService() {
|
||||
super(Collections.singleton(THING_TYPE_DEVICE), (int) TIMEOUT.getSeconds(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
performScan(false);
|
||||
}
|
||||
|
||||
private void performScan(boolean applyTtl) {
|
||||
logger.debug("Scanning directory {} for devices", DEVICE_DIRECTORY);
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
File directory = DEVICE_DIRECTORY.toFile();
|
||||
Duration ttl = null;
|
||||
if (applyTtl) {
|
||||
ttl = REFRESH_INTERVAL.multipliedBy(2);
|
||||
}
|
||||
if (directory == null) {
|
||||
logger.warn("Could not open device directory {}", DEVICE_DIRECTORY);
|
||||
return;
|
||||
}
|
||||
File[] devices = directory.listFiles();
|
||||
if (devices == null) {
|
||||
logger.warn("Could not enumerate {}, it is not a directory", directory);
|
||||
return;
|
||||
}
|
||||
for (File file : devices) {
|
||||
handleFile(file, ttl);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFile(File file, @Nullable Duration ttl) {
|
||||
logger.trace("Discovering file {}", file);
|
||||
if (file.isDirectory()) {
|
||||
logger.trace("{} is not a file, ignoring", file);
|
||||
return;
|
||||
}
|
||||
if (!file.canRead()) {
|
||||
logger.debug("{} is not readable, ignoring", file);
|
||||
return;
|
||||
}
|
||||
DiscoveryResultBuilder result = DiscoveryResultBuilder.create(new ThingUID(THING_TYPE_DEVICE, file.getName()))
|
||||
.withProperty("path", file.getAbsolutePath()).withRepresentationProperty(file.getName());
|
||||
if (ttl != null) {
|
||||
result = result.withTTL(ttl.getSeconds());
|
||||
}
|
||||
|
||||
boolean shouldDiscover = enrichDevice(result, file);
|
||||
if (shouldDiscover) {
|
||||
DiscoveryResult thing = result.build();
|
||||
logger.debug("Discovered: {}", thing);
|
||||
thingDiscovered(thing);
|
||||
} else {
|
||||
logger.debug("{} is not a keyboard, ignoring", file);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean enrichDevice(DiscoveryResultBuilder builder, File inputDevice) {
|
||||
String label = inputDevice.getName();
|
||||
try {
|
||||
try (EvdevDevice device = new EvdevDevice(inputDevice.getAbsolutePath())) {
|
||||
String labelFromDevice = device.getName();
|
||||
boolean isKeyboard = device.has(EvdevLibrary.Type.KEY);
|
||||
if (labelFromDevice != null) {
|
||||
label = labelFromDevice;
|
||||
}
|
||||
return isKeyboard;
|
||||
} finally {
|
||||
builder.withLabel(label);
|
||||
}
|
||||
} catch (IOException | LastErrorException e) {
|
||||
logger.debug("Could not open device {}", inputDevice, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
super.stopScan();
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.debug("Starting background discovery");
|
||||
if (discoveryJob == null || discoveryJob.isCancelled()) {
|
||||
WatchService watchService = null;
|
||||
try {
|
||||
watchService = makeWatcher();
|
||||
} catch (IOException e) {
|
||||
logger.debug("Could not start event based watcher, falling back to polling", e);
|
||||
}
|
||||
if (watchService != null) {
|
||||
WatchService watcher = watchService;
|
||||
FutureTask<?> job = new FutureTask<>(() -> {
|
||||
waitForNewDevices(watcher);
|
||||
return null;
|
||||
});
|
||||
Thread t = Utils.backgroundThread(job, getClass(), null);
|
||||
t.start();
|
||||
discoveryJob = job;
|
||||
} else {
|
||||
discoveryJob = scheduler.scheduleWithFixedDelay(() -> performScan(true), 0,
|
||||
REFRESH_INTERVAL.getSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WatchService makeWatcher() throws IOException {
|
||||
WatchService watchService = FileSystems.getDefault().newWatchService();
|
||||
DEVICE_DIRECTORY.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
|
||||
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
|
||||
return watchService;
|
||||
}
|
||||
|
||||
private void waitForNewDevices(WatchService watchService) {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
boolean gotEvent = waitAndDrainAll(watchService);
|
||||
logger.debug("Input devices changed: {}. Triggering rescan: {}", gotEvent, gotEvent);
|
||||
|
||||
if (gotEvent) {
|
||||
performScan(false);
|
||||
}
|
||||
}
|
||||
logger.debug("Discovery stopped");
|
||||
}
|
||||
|
||||
private static boolean waitAndDrainAll(WatchService watchService) {
|
||||
WatchKey event;
|
||||
try {
|
||||
event = watchService.poll(EVENT_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
do {
|
||||
event.pollEvents();
|
||||
event.reset();
|
||||
event = watchService.poll();
|
||||
} while (event != null);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stopping background discovery");
|
||||
if (discoveryJob != null && !discoveryJob.isCancelled()) {
|
||||
discoveryJob.cancel(true);
|
||||
discoveryJob = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.util.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.EvdevDevice;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.*;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handler for Linux Input devices.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class LinuxInputHandler extends DeviceReadingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LinuxInputHandler.class);
|
||||
|
||||
private final Map<Integer, Channel> channels;
|
||||
private final Channel keyChannel;
|
||||
private @Nullable EvdevDevice device;
|
||||
private final @Nullable String defaultLabel;
|
||||
|
||||
private @NonNullByDefault({}) LinuxInputConfiguration config;
|
||||
|
||||
public LinuxInputHandler(Thing thing, @Nullable String defaultLabel) {
|
||||
super(thing);
|
||||
this.defaultLabel = defaultLabel;
|
||||
|
||||
keyChannel = ChannelBuilder.create(new ChannelUID(thing.getUID(), "key"), CoreItemFactory.STRING)
|
||||
.withType(CHANNEL_TYPE_KEY).build();
|
||||
channels = Collections.synchronizedMap(new HashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
/* no commands to handle */
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean immediateSetup() {
|
||||
config = getConfigAs(LinuxInputConfiguration.class);
|
||||
channels.clear();
|
||||
String statusDesc = null;
|
||||
if (!config.enable) {
|
||||
statusDesc = "Administratively disabled";
|
||||
}
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, statusDesc);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean delayedSetup() throws IOException {
|
||||
ThingBuilder customizer = editThing();
|
||||
List<Channel> newChannels = new ArrayList<>();
|
||||
newChannels.add(keyChannel);
|
||||
EvdevDevice newDevice = new EvdevDevice(config.path);
|
||||
for (EvdevDevice.Key o : newDevice.enumerateKeys()) {
|
||||
String name = o.getName();
|
||||
Channel channel = ChannelBuilder
|
||||
.create(new ChannelUID(thing.getUID(), CHANNEL_GROUP_KEYPRESSES_ID, name), CoreItemFactory.CONTACT)
|
||||
.withLabel(name).withType(CHANNEL_TYPE_KEY_PRESS).build();
|
||||
channels.put(o.getCode(), channel);
|
||||
newChannels.add(channel);
|
||||
}
|
||||
if (Objects.equals(defaultLabel, thing.getLabel())) {
|
||||
customizer.withLabel(newDevice.getName());
|
||||
}
|
||||
customizer.withChannels(newChannels);
|
||||
Map<String, String> props = getProperties(Objects.requireNonNull(newDevice));
|
||||
customizer.withProperties(props);
|
||||
updateThing(customizer.build());
|
||||
for (Channel channel : newChannels) {
|
||||
updateState(channel.getUID(), OpenClosedType.OPEN);
|
||||
}
|
||||
if (config.enable) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
device = newDevice;
|
||||
return config.enable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeDevice() throws IOException {
|
||||
@Nullable
|
||||
EvdevDevice currentDevice = device;
|
||||
device = null;
|
||||
|
||||
if (currentDevice != null) {
|
||||
currentDevice.close();
|
||||
}
|
||||
logger.debug("Device {} closed", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getInstanceName() {
|
||||
LinuxInputConfiguration c = config;
|
||||
if (c == null || c.path == null) {
|
||||
return "unknown";
|
||||
}
|
||||
return c.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
void handleEventsInThread() throws IOException {
|
||||
try (Selector selector = EvdevDevice.openSelector()) {
|
||||
@Nullable
|
||||
EvdevDevice currentDevice = device;
|
||||
if (currentDevice == null) {
|
||||
throw new IOException("trying to handle events without an device");
|
||||
}
|
||||
SelectionKey evdevReady = currentDevice.register(selector);
|
||||
|
||||
logger.debug("Grabbing device {}", currentDevice);
|
||||
currentDevice.grab(); // ungrab will happen implicitly at device.close()
|
||||
|
||||
while (true) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
logger.debug("Thread interrupted, exiting");
|
||||
break;
|
||||
}
|
||||
logger.trace("Waiting for event");
|
||||
selector.select(20_000);
|
||||
if (selector.selectedKeys().remove(evdevReady)) {
|
||||
while (true) {
|
||||
Optional<EvdevDevice.InputEvent> ev = currentDevice.nextEvent();
|
||||
if (!ev.isPresent()) {
|
||||
break;
|
||||
}
|
||||
handleEvent(ev.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEvent(EvdevDevice.InputEvent event) {
|
||||
if (event.type() != EvdevLibrary.Type.KEY) {
|
||||
return;
|
||||
}
|
||||
@Nullable
|
||||
Channel channel = channels.get(event.getCode());
|
||||
if (channel == null) {
|
||||
String msg = "Could not find channel for code {}";
|
||||
if (isInitialized()) {
|
||||
logger.warn(msg, event.getCode());
|
||||
} else {
|
||||
logger.debug(msg, event.getCode());
|
||||
}
|
||||
return;
|
||||
}
|
||||
logger.debug("Got event: {}", event);
|
||||
// Documented in README.md
|
||||
int eventValue = event.getValue();
|
||||
switch (eventValue) {
|
||||
case EvdevLibrary.KeyEventValue.DOWN:
|
||||
String keyCode = channel.getUID().getIdWithoutGroup();
|
||||
updateState(keyChannel.getUID(), new StringType(keyCode));
|
||||
updateState(channel.getUID(), OpenClosedType.CLOSED);
|
||||
triggerChannel(keyChannel.getUID(), keyCode);
|
||||
triggerChannel(channel.getUID(), CommonTriggerEvents.PRESSED);
|
||||
updateState(keyChannel.getUID(), new StringType());
|
||||
break;
|
||||
case EvdevLibrary.KeyEventValue.UP:
|
||||
updateState(channel.getUID(), OpenClosedType.OPEN);
|
||||
triggerChannel(channel.getUID(), CommonTriggerEvents.RELEASED);
|
||||
break;
|
||||
case EvdevLibrary.KeyEventValue.REPEAT:
|
||||
/* Ignored */
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unexpected event value for channel {}: {}", channel, eventValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> getProperties(EvdevDevice device) {
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put("physicalLocation", device.getPhys());
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getUniq());
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, hex(device.getProdutId()));
|
||||
properties.put(Thing.PROPERTY_VENDOR, hex(device.getVendorId()));
|
||||
properties.put("busType", device.getBusType().map(Object::toString).orElseGet(() -> hex(device.getBusId())));
|
||||
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, hex(device.getVersionId()));
|
||||
properties.put("driverVersion", hex(device.getDriverVersion()));
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static String hex(int i) {
|
||||
return String.format("%04x", i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.THING_TYPE_DEVICE;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* InputHandlerFactory for Linux Input devices.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.linuxinput", service = ThingHandlerFactory.class)
|
||||
public class LinuxInputHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_DEVICE);
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_DEVICE.equals(thingTypeUID)) {
|
||||
return new LinuxInputHandler(thing, getThingTypeByUID(thing.getThingTypeUID()).getLabel());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utilities
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class Utils {
|
||||
private Utils() {
|
||||
}
|
||||
|
||||
static Thread backgroundThread(Runnable r, Class<?> clazz, @Nullable String instance) {
|
||||
String name = LinuxInputBindingConstants.BINDING_ID + " :: " + clazz.getSimpleName();
|
||||
if (instance != null) {
|
||||
name += " :: " + instance;
|
||||
}
|
||||
Thread t = new Thread(r, name);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal.evdev4j;
|
||||
|
||||
import static org.openhab.binding.linuxinput.internal.evdev4j.Utils.combineFlags;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.SelectableChannel;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.spi.SelectorProvider;
|
||||
import java.text.MessageFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
|
||||
|
||||
import jnr.constants.platform.Errno;
|
||||
import jnr.constants.platform.OpenFlags;
|
||||
import jnr.enxio.channels.NativeDeviceChannel;
|
||||
import jnr.enxio.channels.NativeFileSelectorProvider;
|
||||
import jnr.ffi.byref.PointerByReference;
|
||||
import jnr.posix.POSIX;
|
||||
import jnr.posix.POSIXFactory;
|
||||
|
||||
/**
|
||||
* Classbased access to libevdev-input functionality.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EvdevDevice implements Closeable {
|
||||
private static final SelectorProvider SELECTOR_PROVIDER = NativeFileSelectorProvider.getInstance();
|
||||
|
||||
private final EvdevLibrary lib = EvdevLibrary.load();
|
||||
private final POSIX posix = POSIXFactory.getNativePOSIX();
|
||||
private final SelectableChannel channel;
|
||||
private final EvdevLibrary.Handle handle;
|
||||
|
||||
public EvdevDevice(String path) throws IOException {
|
||||
int fd = posix.open(path, combineFlags(OpenFlags.class, OpenFlags.O_RDONLY, OpenFlags.O_CLOEXEC), 0);
|
||||
if (fd == -1) {
|
||||
throw new LastErrorException(posix, posix.errno(), path);
|
||||
}
|
||||
|
||||
EvdevLibrary.Handle newHandle = EvdevLibrary.makeHandle(lib);
|
||||
PointerByReference ref = new PointerByReference();
|
||||
int ret = lib.new_from_fd(fd, ref);
|
||||
if (ret != 0) {
|
||||
throw new LastErrorException(posix, -ret);
|
||||
}
|
||||
newHandle.useMemory(ref.getValue());
|
||||
handle = newHandle;
|
||||
NativeDeviceChannel newChannel = new NativeDeviceChannel(SELECTOR_PROVIDER, fd, SelectionKey.OP_READ, true);
|
||||
newChannel.configureBlocking(false);
|
||||
channel = newChannel;
|
||||
}
|
||||
|
||||
private void grab(EvdevLibrary.GrabMode mode) {
|
||||
int ret = lib.grab(handle, mode.getValue());
|
||||
if (ret != 0) {
|
||||
throw new LastErrorException(posix, -ret);
|
||||
}
|
||||
}
|
||||
|
||||
public void grab() {
|
||||
grab(EvdevLibrary.GrabMode.GRAB);
|
||||
}
|
||||
|
||||
public void ungrab() {
|
||||
grab(EvdevLibrary.GrabMode.UNGRAB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MessageFormat.format("Evdev {0}|{1}|{2}", lib.get_name(handle), lib.get_phys(handle),
|
||||
lib.get_uniq(handle));
|
||||
}
|
||||
|
||||
public Optional<InputEvent> nextEvent() {
|
||||
// EV_SYN/SYN_DROPPED handling?
|
||||
EvdevLibrary.InputEvent event = new EvdevLibrary.InputEvent(jnr.ffi.Runtime.getRuntime(lib));
|
||||
int ret = lib.next_event(handle, EvdevLibrary.ReadFlag.NORMAL, event);
|
||||
if (ret == -Errno.EAGAIN.intValue()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (ret < 0) {
|
||||
throw new LastErrorException(posix, -ret);
|
||||
}
|
||||
return Optional.of(new InputEvent(lib, Instant.ofEpochSecond(event.sec.get(), event.usec.get()),
|
||||
event.type.get(), event.code.get(), event.value.get()));
|
||||
}
|
||||
|
||||
public static Selector openSelector() throws IOException {
|
||||
return SELECTOR_PROVIDER.openSelector();
|
||||
}
|
||||
|
||||
public SelectionKey register(Selector selector) throws ClosedChannelException {
|
||||
return channel.register(selector, SelectionKey.OP_READ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
lib.free(handle);
|
||||
channel.close();
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
public String getName() {
|
||||
return lib.get_name(handle);
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
lib.set_name(handle, name);
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
public String getPhys() {
|
||||
return lib.get_phys(handle);
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
public String getUniq() {
|
||||
return lib.get_uniq(handle);
|
||||
}
|
||||
|
||||
public int getProdutId() {
|
||||
return lib.get_id_product(handle);
|
||||
}
|
||||
|
||||
public int getVendorId() {
|
||||
return lib.get_id_vendor(handle);
|
||||
}
|
||||
|
||||
public int getBusId() {
|
||||
return lib.get_id_bustype(handle);
|
||||
}
|
||||
|
||||
public Optional<EvdevLibrary.BusType> getBusType() {
|
||||
return EvdevLibrary.BusType.fromInt(getBusId());
|
||||
}
|
||||
|
||||
public int getVersionId() {
|
||||
return lib.get_id_version(handle);
|
||||
}
|
||||
|
||||
public int getDriverVersion() {
|
||||
return lib.get_driver_version(handle);
|
||||
}
|
||||
|
||||
public boolean has(EvdevLibrary.Type type) {
|
||||
return lib.has_event_type(handle, type.intValue());
|
||||
}
|
||||
|
||||
public boolean has(EvdevLibrary.Type type, int code) {
|
||||
return lib.has_event_code(handle, type.intValue(), code);
|
||||
}
|
||||
|
||||
public void enable(EvdevLibrary.Type type) {
|
||||
int result = lib.enable_event_type(handle, type.intValue());
|
||||
if (result != 0) {
|
||||
throw new FailedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public void enable(EvdevLibrary.Type type, int code) {
|
||||
int result = lib.enable_event_code(handle, type.intValue(), code);
|
||||
if (result != 0) {
|
||||
throw new FailedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<Key> enumerateKeys() {
|
||||
int minKey = 0;
|
||||
int maxKey = 255 - 1;
|
||||
List<Key> result = new ArrayList<>();
|
||||
for (int i = minKey; i <= maxKey; i++) {
|
||||
if (has(EvdevLibrary.Type.KEY, i)) {
|
||||
result.add(new Key(lib, i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Key {
|
||||
private final EvdevLibrary lib;
|
||||
private final int code;
|
||||
|
||||
private Key(EvdevLibrary lib, int code) {
|
||||
this.lib = lib;
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return lib.event_code_get_name(EvdevLibrary.Type.KEY.intValue(), code);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InputEvent {
|
||||
private EvdevLibrary lib;
|
||||
|
||||
private final Instant time;
|
||||
private final int type;
|
||||
private final int code;
|
||||
private final int value;
|
||||
|
||||
private InputEvent(EvdevLibrary lib, Instant time, int type, int code, int value) {
|
||||
this.lib = lib;
|
||||
this.time = time;
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Instant getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public EvdevLibrary.Type type() {
|
||||
return EvdevLibrary.Type.fromInt(type)
|
||||
.orElseThrow(() -> new IllegalStateException("Could not find 'Type' for value " + type));
|
||||
}
|
||||
|
||||
public Optional<String> typeName() {
|
||||
return Optional.ofNullable(lib.event_type_get_name(type));
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public Optional<String> codeName() {
|
||||
return Optional.ofNullable(lib.event_code_get_name(type, code));
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Optional<String> valueName() {
|
||||
return Optional.ofNullable(lib.event_value_get_name(type, code, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MessageFormat.format("{0}: {1}/{2}/{3}", DateTimeFormatter.ISO_INSTANT.format(time),
|
||||
typeName().orElse(String.valueOf(type)), codeName().orElse(String.valueOf(code)),
|
||||
valueName().orElse(String.valueOf(value)));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FailedOperationException extends RuntimeException {
|
||||
private static final long serialVersionUID = -8556632931670798678L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal.evdev4j;
|
||||
|
||||
import static org.openhab.binding.linuxinput.internal.evdev4j.Utils.constantFromInt;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import jnr.constants.platform.linux.Errno;
|
||||
import jnr.posix.POSIX;
|
||||
|
||||
/**
|
||||
* Exception wrapping an operating system errno.
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LastErrorException extends RuntimeException {
|
||||
private static final long serialVersionUID = 3112920209797990207L;
|
||||
private final int errno;
|
||||
|
||||
LastErrorException(POSIX posix, int errno) {
|
||||
super("Error " + errno + ": " + posix.strerror(errno));
|
||||
this.errno = errno;
|
||||
}
|
||||
|
||||
LastErrorException(POSIX posix, int errno, String detail) {
|
||||
super(MessageFormat.format("Error ({0}) for {1}: {2}", errno, detail, posix.strerror(errno)));
|
||||
this.errno = errno;
|
||||
}
|
||||
|
||||
public int getErrno() {
|
||||
return errno;
|
||||
}
|
||||
|
||||
public Optional<Errno> getError() {
|
||||
return constantFromInt(Errno.values(), errno);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal.evdev4j;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import jnr.constants.Constant;
|
||||
|
||||
/**
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Utils {
|
||||
private Utils() {
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
static <T extends Constant> int combineFlags(Class<T> klazz, T... flags) {
|
||||
if (klazz == Constant.class) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
int result = 0;
|
||||
for (Constant c : flags) {
|
||||
result |= c.intValue();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T extends Constant> Optional<T> constantFromInt(T[] cs, int i) {
|
||||
for (T c : cs) {
|
||||
if (c.intValue() == i) {
|
||||
return Optional.of(c);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal.evdev4j.jnr;
|
||||
|
||||
import static org.openhab.binding.linuxinput.internal.evdev4j.Utils.constantFromInt;
|
||||
import static org.openhab.binding.linuxinput.internal.evdev4j.jnr.Utils.trimEnd;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.linuxinput.internal.evdev4j.Utils;
|
||||
|
||||
import jnr.constants.Constant;
|
||||
import jnr.ffi.LibraryLoader;
|
||||
import jnr.ffi.Runtime;
|
||||
import jnr.ffi.Struct;
|
||||
import jnr.ffi.annotations.IgnoreError;
|
||||
import jnr.ffi.annotations.In;
|
||||
import jnr.ffi.annotations.Out;
|
||||
import jnr.ffi.byref.PointerByReference;
|
||||
import jnr.ffi.mapper.FunctionMapper;
|
||||
|
||||
/**
|
||||
* JNR library for libdevdev library (libevdev.h).
|
||||
*
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@IgnoreError
|
||||
public interface EvdevLibrary {
|
||||
static EvdevLibrary load() {
|
||||
FunctionMapper evdevFunctionMapper = (functionName, context) -> "libevdev_" + trimEnd("_", functionName);
|
||||
return LibraryLoader.create(EvdevLibrary.class).library("evdev").mapper(evdevFunctionMapper).load();
|
||||
}
|
||||
|
||||
static Handle makeHandle(EvdevLibrary lib) {
|
||||
return new Handle(Runtime.getRuntime(lib));
|
||||
}
|
||||
|
||||
class Handle extends Struct {
|
||||
public Handle(Runtime runtime) {
|
||||
super(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
enum GrabMode {
|
||||
GRAB(3),
|
||||
UNGRAB(4);
|
||||
|
||||
private int value;
|
||||
|
||||
GrabMode(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
final class InputEvent extends Struct {
|
||||
public UnsignedLong sec = new UnsignedLong();
|
||||
public UnsignedLong usec = new UnsignedLong();
|
||||
public Unsigned16 type = new Unsigned16();
|
||||
public Unsigned16 code = new Unsigned16();
|
||||
public Signed32 value = new Signed32();
|
||||
|
||||
public InputEvent(Runtime runtime) {
|
||||
super(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
int new_from_fd(int fd, @Out PointerByReference handle);
|
||||
|
||||
void free(@In Handle handle);
|
||||
|
||||
int grab(@In Handle handle, int grab);
|
||||
|
||||
int next_event(@In Handle handle, int flags, @Out InputEvent event);
|
||||
|
||||
String event_type_get_name(int type);
|
||||
|
||||
String event_code_get_name(int type, int code);
|
||||
|
||||
String event_value_get_name(int type, int code, int value);
|
||||
|
||||
boolean has_event_type(@In Handle handle, int type);
|
||||
|
||||
int enable_event_type(@In Handle handle, int type);
|
||||
|
||||
int disable_event_type(@In Handle handle, int type);
|
||||
|
||||
boolean has_event_code(@In Handle handle, int type, int code);
|
||||
|
||||
int enable_event_code(@In Handle handle, int type, int code);
|
||||
|
||||
int disable_event_code(@In Handle handle, int type, int code);
|
||||
|
||||
String get_name(@In Handle handle);
|
||||
|
||||
void set_name(@In Handle handle, String name);
|
||||
|
||||
String get_phys(@In Handle handle);
|
||||
|
||||
String get_uniq(@In Handle handle);
|
||||
|
||||
int get_id_product(@In Handle handle);
|
||||
|
||||
int get_id_vendor(@In Handle handle);
|
||||
|
||||
int get_id_bustype(@In Handle handle);
|
||||
|
||||
int get_id_version(@In Handle handle);
|
||||
|
||||
int get_driver_version(@In Handle handle);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class ReadFlag {
|
||||
private ReadFlag() {
|
||||
}
|
||||
|
||||
public static final int SYNC = 1;
|
||||
public static final int NORMAL = 2;
|
||||
public static final int FORCE_SYNC = 4;
|
||||
public static final int BLOCKING = 8;
|
||||
}
|
||||
|
||||
class KeyEventValue {
|
||||
private KeyEventValue() {
|
||||
}
|
||||
|
||||
public static final int UP = 0;
|
||||
public static final int DOWN = 1;
|
||||
public static final int REPEAT = 2;
|
||||
}
|
||||
|
||||
enum Type implements Constant {
|
||||
SYN(0x00),
|
||||
KEY(0x01),
|
||||
REL(0x02),
|
||||
ABS(0x03),
|
||||
MSC(0x04),
|
||||
SW(0x05),
|
||||
LED(0x11),
|
||||
SND(0x12),
|
||||
REP(0x14),
|
||||
FF(0x15),
|
||||
PWR(0x16),
|
||||
FF_STATUS(0x17),
|
||||
MAX(0x1f),
|
||||
CNT(0x20);
|
||||
|
||||
private final int i;
|
||||
|
||||
Type(int i) {
|
||||
this.i = i;
|
||||
}
|
||||
|
||||
public static Optional<Type> fromInt(int i) {
|
||||
return constantFromInt(Type.values(), i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long longValue() {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean defined() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enum BusType implements Constant {
|
||||
PCI(0x01),
|
||||
ISAPNP(0x02),
|
||||
USB(0x03),
|
||||
HIL(0x04),
|
||||
BLUETOOTH(0x05),
|
||||
VIRTUAL(0x06),
|
||||
|
||||
ISA(0x10),
|
||||
I8042(0x11),
|
||||
XTKBD(0x12),
|
||||
RS232(0x13),
|
||||
GAMEPORT(0x14),
|
||||
PARPORT(0x15),
|
||||
AMIGA(0x16),
|
||||
ADB(0x17),
|
||||
I2C(0x18),
|
||||
HOST(0x19),
|
||||
GSC(0x1A),
|
||||
ATARI(0x1B),
|
||||
SPI(0x1C),
|
||||
RMI(0x1D),
|
||||
CEC(0x1E),
|
||||
INTEL_ISHTP(0x1F);
|
||||
|
||||
private final int i;
|
||||
|
||||
BusType(int i) {
|
||||
this.i = i;
|
||||
}
|
||||
|
||||
public static Optional<BusType> fromInt(int i) {
|
||||
return Utils.constantFromInt(BusType.values(), i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long longValue() {
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean defined() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.linuxinput.internal.evdev4j.jnr;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Thomas Weißschuh - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class Utils {
|
||||
private Utils() {
|
||||
}
|
||||
|
||||
static String trimEnd(String suffix, String s) {
|
||||
if (s.endsWith(suffix)) {
|
||||
return s.substring(0, s.length() - suffix.length());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="linuxinput" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>LinuxInput Binding</name>
|
||||
<description>This is the binding for LinuxInput.</description>
|
||||
<author>Thomas Weißschuh</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="linuxinput"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="input-device">
|
||||
<label>LinuxInput Device</label>
|
||||
<description>Input device</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="keypresses" typeId="keypresses"/>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="enable" type="boolean" required="true">
|
||||
<label>Enable</label>
|
||||
<description>If the Thing should be enabled and consume all input from this device</description>
|
||||
</parameter>
|
||||
<parameter name="path" type="text" required="true">
|
||||
<label>Path</label>
|
||||
<description>Path to device file</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="key">
|
||||
<item-type>String</item-type>
|
||||
<label>Key Event</label>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="keypress">
|
||||
<item-type>Contact</item-type>
|
||||
<label>Key Pressed</label>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="device-grab">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Device Grab</label>
|
||||
<category>Switch</category>
|
||||
<tags>
|
||||
<tag>Switchable</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
|
||||
<channel-group-type id="keypresses">
|
||||
<label>Key Presses</label>
|
||||
</channel-group-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user