2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.linuxinput.internal;
15 import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.*;
17 import java.io.IOException;
18 import java.nio.channels.SelectionKey;
19 import java.nio.channels.Selector;
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.Objects;
26 import java.util.Optional;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.linuxinput.internal.evdev4j.EvdevDevice;
31 import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
32 import org.openhab.core.library.CoreItemFactory;
33 import org.openhab.core.library.types.OpenClosedType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Channel;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.CommonTriggerEvents;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.builder.ChannelBuilder;
42 import org.openhab.core.thing.binding.builder.ThingBuilder;
43 import org.openhab.core.types.Command;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * Handler for Linux Input devices.
50 * @author Thomas Weißschuh - Initial contribution
53 public final class LinuxInputHandler extends DeviceReadingHandler {
55 private final Logger logger = LoggerFactory.getLogger(LinuxInputHandler.class);
57 private final Map<Integer, Channel> channels;
58 private final Channel keyChannel;
59 private @Nullable EvdevDevice device;
60 private final @Nullable String defaultLabel;
62 private @NonNullByDefault({}) LinuxInputConfiguration config;
64 public LinuxInputHandler(Thing thing, @Nullable String defaultLabel) {
66 this.defaultLabel = defaultLabel;
68 keyChannel = ChannelBuilder.create(new ChannelUID(thing.getUID(), "key"), CoreItemFactory.STRING)
69 .withType(CHANNEL_TYPE_KEY).build();
70 channels = Collections.synchronizedMap(new HashMap<>());
74 public void handleCommand(ChannelUID channelUID, Command command) {
75 /* no commands to handle */
79 boolean immediateSetup() {
80 config = getConfigAs(LinuxInputConfiguration.class);
82 String statusDesc = null;
84 statusDesc = "Administratively disabled";
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, statusDesc);
91 boolean delayedSetup() throws IOException {
92 ThingBuilder customizer = editThing();
93 List<Channel> newChannels = new ArrayList<>();
94 newChannels.add(keyChannel);
95 EvdevDevice newDevice = new EvdevDevice(config.path);
96 for (EvdevDevice.Key o : newDevice.enumerateKeys()) {
97 String name = o.getName();
98 Channel channel = ChannelBuilder
99 .create(new ChannelUID(thing.getUID(), CHANNEL_GROUP_KEYPRESSES_ID, name), CoreItemFactory.CONTACT)
100 .withLabel(name).withType(CHANNEL_TYPE_KEY_PRESS).build();
101 channels.put(o.getCode(), channel);
102 newChannels.add(channel);
104 if (Objects.equals(defaultLabel, thing.getLabel())) {
105 customizer.withLabel(newDevice.getName());
107 customizer.withChannels(newChannels);
108 Map<String, String> props = getProperties(Objects.requireNonNull(newDevice));
109 customizer.withProperties(props);
110 updateThing(customizer.build());
111 for (Channel channel : newChannels) {
112 updateState(channel.getUID(), OpenClosedType.OPEN);
115 updateStatus(ThingStatus.ONLINE);
118 return config.enable;
122 protected void closeDevice() throws IOException {
124 EvdevDevice currentDevice = device;
127 if (currentDevice != null) {
128 currentDevice.close();
130 logger.debug("Device {} closed", this);
134 String getInstanceName() {
135 LinuxInputConfiguration c = config;
136 if (c == null || c.path == null) {
143 void handleEventsInThread() throws IOException {
144 try (Selector selector = EvdevDevice.openSelector()) {
146 EvdevDevice currentDevice = device;
147 if (currentDevice == null) {
148 throw new IOException("trying to handle events without an device");
150 SelectionKey evdevReady = currentDevice.register(selector);
152 logger.debug("Grabbing device {}", currentDevice);
153 currentDevice.grab(); // ungrab will happen implicitly at device.close()
156 if (Thread.currentThread().isInterrupted()) {
157 logger.debug("Thread interrupted, exiting");
160 logger.trace("Waiting for event");
161 selector.select(20_000);
162 if (selector.selectedKeys().remove(evdevReady)) {
164 Optional<EvdevDevice.InputEvent> ev = currentDevice.nextEvent();
165 if (!ev.isPresent()) {
168 handleEvent(ev.get());
175 private void handleEvent(EvdevDevice.InputEvent event) {
176 if (event.type() != EvdevLibrary.Type.KEY) {
180 Channel channel = channels.get(event.getCode());
181 if (channel == null) {
182 String msg = "Could not find channel for code {}";
183 if (isInitialized()) {
184 logger.warn(msg, event.getCode());
186 logger.debug(msg, event.getCode());
190 logger.debug("Got event: {}", event);
191 // Documented in README.md
192 int eventValue = event.getValue();
193 switch (eventValue) {
194 case EvdevLibrary.KeyEventValue.DOWN:
195 String keyCode = channel.getUID().getIdWithoutGroup();
196 updateState(keyChannel.getUID(), new StringType(keyCode));
197 updateState(channel.getUID(), OpenClosedType.CLOSED);
198 triggerChannel(keyChannel.getUID(), keyCode);
199 triggerChannel(channel.getUID(), CommonTriggerEvents.PRESSED);
200 updateState(keyChannel.getUID(), new StringType());
202 case EvdevLibrary.KeyEventValue.UP:
203 updateState(channel.getUID(), OpenClosedType.OPEN);
204 triggerChannel(channel.getUID(), CommonTriggerEvents.RELEASED);
206 case EvdevLibrary.KeyEventValue.REPEAT:
210 logger.debug("Unexpected event value for channel {}: {}", channel, eventValue);
215 private static Map<String, String> getProperties(EvdevDevice device) {
216 Map<String, String> properties = new HashMap<>();
217 properties.put("physicalLocation", device.getPhys());
218 properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getUniq());
219 properties.put(Thing.PROPERTY_MODEL_ID, hex(device.getProdutId()));
220 properties.put(Thing.PROPERTY_VENDOR, hex(device.getVendorId()));
221 properties.put("busType", device.getBusType().map(Object::toString).orElseGet(() -> hex(device.getBusId())));
222 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, hex(device.getVersionId()));
223 properties.put("driverVersion", hex(device.getDriverVersion()));
227 private static String hex(int i) {
228 return String.format("%04x", i);