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,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.linuxinput</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,83 @@
# Linux Input Binding
This binding allows to you use a keyboard to control your openHAB instance.
It works by exposing all keys on the keyboard as channels.
As indicated by the name of the binding this only works on Linux.
(It uses the libevdev library the receive events from the kernel)
As all the low-level protocols are handled by the Linux kernel it works for any
kind of keyboard; USB, Bluetooth, etc.
## Supported Things
All keyboards supported by the Linux kernel.
## Discovery
The discovery feature finds all cold- or hotplugged keyboards by watching the
`/dev/input/` directory.
The discovery uses the numeric ids for the devices. (`/dev/input/event0`,
etc...).
This can lead to issues when the kernel autodiscovery enumerates devices in a
nondeterministic order. This problem can be circumvented by using predictable
device names in `/dev/input/by-id/` or `/dev/input/by-path/` or by using Udev
facilities (out of scope for this document).
## Binding Configuration
openHAB will need rights on the `/dev/input/` files it is supposed to access.
This can be implemented by group-memberships, (custom) initscripts or Udev
rules.
The exact configurations possible depend on your system (out of scope for this document).
The `libevdev` library has to be installed for this plugin to work.
(It should be available via your package manager)
## Thing Configuration
Each thing has has to be explicitly enabled after it is configured.
While it is enabled *all* of the generated events will be consumed by openHAB.
The device will not be available for normal input processing!
### Static configuration
#### Thing
```
Thing linuxinput:input-device:some-keyboard [ enable=true, path="/dev/input/eventXX" ]
```
#### Item
```
Contact SomeButton "Some Button" { channel="linuxinput:input-device:event17:keypresses#KEY_0" }
```
## Channels
Each Thing provides multiple channels
* A `key` channel that aggregates all events.
* Per physical key channels.
### Events
The following happens when pressing and releasing a key:
#### Press
1) State of global key channel updated to new key.
2) State of per-key channel updated to `"CLOSED"`.
3) Global key channel triggered with the current key name.
4) Per-key channel triggered with `"PRESSED"`".
5) State of global key channel updated to `""` (Empty string)
#### Release
1) State of per-key channel updated to `"OPEN"`
2) Per-key channel triggered with `"RELEASED"`
#### Rationale
Channel states are updated first to allow rules triggered by channel triggers to access the new state.

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.linuxinput</artifactId>
<name>openHAB Add-ons :: Bundles :: LinuxInput Binding</name>
<properties>
<bnd.importpackage>jnr.ffi.provider;version=!,jnr.ffi.provider.jffi;version=!,com.kenai.jffi;version=!,jnr.ffi.provider.converters</bnd.importpackage>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jffi</artifactId>
<version>1.2.18</version>
<scope>compile</scope>
</dependency>
<!-- we need this for RPi hardfloat abi -->
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jffi</artifactId>
<version>1.2.18</version>
<classifier>native</classifier>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-enxio</artifactId>
<version>0.19</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-posix</artifactId>
<version>3.0.47</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

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>