added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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&amp;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&amp;Bundle-SymbolicName=com.github.jnr.jnr-a64asm&amp;Bundle-Version=1.0.0</bundle>
<bundle dependency="true">wrap:mvn:com.github.jnr/jnr-x86asm/1.0.2$Bundle-Name=jnr-x86asm&amp;Bundle-SymbolicName=com.github.jnr.jnr-x86asm&amp;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>

View File

@@ -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");
}
}
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>