2 * Copyright (c) 2010-2023 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();
99 name = Integer.toString(o.getCode());
101 Channel channel = ChannelBuilder
102 .create(new ChannelUID(thing.getUID(), CHANNEL_GROUP_KEYPRESSES_ID, name), CoreItemFactory.CONTACT)
103 .withLabel(name).withType(CHANNEL_TYPE_KEY_PRESS).withDescription("Event Code " + o.getCode())
105 channels.put(o.getCode(), channel);
106 newChannels.add(channel);
108 if (Objects.equals(defaultLabel, thing.getLabel())) {
109 customizer.withLabel(newDevice.getName());
111 customizer.withChannels(newChannels);
112 Map<String, String> props = getProperties(Objects.requireNonNull(newDevice));
113 customizer.withProperties(props);
114 updateThing(customizer.build());
115 for (Channel channel : newChannels) {
116 updateState(channel.getUID(), OpenClosedType.OPEN);
119 updateStatus(ThingStatus.ONLINE);
122 return config.enable;
126 protected void closeDevice() throws IOException {
128 EvdevDevice currentDevice = device;
131 if (currentDevice != null) {
132 currentDevice.close();
134 logger.debug("Device {} closed", this);
138 String getInstanceName() {
139 LinuxInputConfiguration c = config;
140 if (c == null || c.path == null) {
147 void handleEventsInThread() throws IOException {
148 try (Selector selector = EvdevDevice.openSelector()) {
150 EvdevDevice currentDevice = device;
151 if (currentDevice == null) {
152 throw new IOException("trying to handle events without a device");
154 SelectionKey evdevReady = currentDevice.register(selector);
156 logger.debug("Grabbing device {}", currentDevice);
157 currentDevice.grab(); // ungrab will happen implicitly at device.close()
160 if (Thread.currentThread().isInterrupted()) {
161 logger.debug("Thread interrupted, exiting");
164 logger.trace("Waiting for event");
165 selector.select(20_000);
166 if (selector.selectedKeys().remove(evdevReady)) {
168 Optional<EvdevDevice.InputEvent> ev = currentDevice.nextEvent();
172 handleEvent(ev.get());
179 private void handleEvent(EvdevDevice.InputEvent event) {
180 if (event.type() != EvdevLibrary.Type.KEY) {
184 Channel channel = channels.get(event.getCode());
185 if (channel == null) {
186 String msg = "Could not find channel for code {}";
187 if (isInitialized()) {
188 logger.warn(msg, event.getCode());
190 logger.debug(msg, event.getCode());
194 logger.debug("Got event: {}", event);
195 // Documented in README.md
196 int eventValue = event.getValue();
197 switch (eventValue) {
198 case EvdevLibrary.KeyEventValue.DOWN:
199 String keyCode = channel.getUID().getIdWithoutGroup();
200 updateState(keyChannel.getUID(), new StringType(keyCode));
201 updateState(channel.getUID(), OpenClosedType.CLOSED);
202 triggerChannel(keyChannel.getUID(), keyCode);
203 triggerChannel(channel.getUID(), CommonTriggerEvents.PRESSED);
204 updateState(keyChannel.getUID(), new StringType());
206 case EvdevLibrary.KeyEventValue.UP:
207 updateState(channel.getUID(), OpenClosedType.OPEN);
208 triggerChannel(channel.getUID(), CommonTriggerEvents.RELEASED);
210 case EvdevLibrary.KeyEventValue.REPEAT:
214 logger.debug("Unexpected event value for channel {}: {}", channel, eventValue);
219 private static Map<String, String> getProperties(EvdevDevice device) {
220 Map<String, String> properties = new HashMap<>();
221 properties.put("physicalLocation", device.getPhys());
222 properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getUniq());
223 properties.put(Thing.PROPERTY_MODEL_ID, hex(device.getProdutId()));
224 properties.put(Thing.PROPERTY_VENDOR, hex(device.getVendorId()));
225 properties.put("busType", device.getBusType().map(Object::toString).orElseGet(() -> hex(device.getBusId())));
226 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, hex(device.getVersionId()));
227 properties.put("driverVersion", hex(device.getDriverVersion()));
231 private static String hex(int i) {
232 return String.format("%04x", i);