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;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.linuxinput.internal.evdev4j.EvdevDevice;
25 import org.openhab.binding.linuxinput.internal.evdev4j.jnr.EvdevLibrary;
26 import org.openhab.core.library.CoreItemFactory;
27 import org.openhab.core.library.types.OpenClosedType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.thing.*;
30 import org.openhab.core.thing.binding.builder.ChannelBuilder;
31 import org.openhab.core.thing.binding.builder.ThingBuilder;
32 import org.openhab.core.types.Command;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * Handler for Linux Input devices.
39 * @author Thomas Weißschuh - Initial contribution
42 public final class LinuxInputHandler extends DeviceReadingHandler {
44 private final Logger logger = LoggerFactory.getLogger(LinuxInputHandler.class);
46 private final Map<Integer, Channel> channels;
47 private final Channel keyChannel;
48 private @Nullable EvdevDevice device;
49 private final @Nullable String defaultLabel;
51 private @NonNullByDefault({}) LinuxInputConfiguration config;
53 public LinuxInputHandler(Thing thing, @Nullable String defaultLabel) {
55 this.defaultLabel = defaultLabel;
57 keyChannel = ChannelBuilder.create(new ChannelUID(thing.getUID(), "key"), CoreItemFactory.STRING)
58 .withType(CHANNEL_TYPE_KEY).build();
59 channels = Collections.synchronizedMap(new HashMap<>());
63 public void handleCommand(ChannelUID channelUID, Command command) {
64 /* no commands to handle */
68 boolean immediateSetup() {
69 config = getConfigAs(LinuxInputConfiguration.class);
71 String statusDesc = null;
73 statusDesc = "Administratively disabled";
75 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, statusDesc);
80 boolean delayedSetup() throws IOException {
81 ThingBuilder customizer = editThing();
82 List<Channel> newChannels = new ArrayList<>();
83 newChannels.add(keyChannel);
84 EvdevDevice newDevice = new EvdevDevice(config.path);
85 for (EvdevDevice.Key o : newDevice.enumerateKeys()) {
86 String name = o.getName();
87 Channel channel = ChannelBuilder
88 .create(new ChannelUID(thing.getUID(), CHANNEL_GROUP_KEYPRESSES_ID, name), CoreItemFactory.CONTACT)
89 .withLabel(name).withType(CHANNEL_TYPE_KEY_PRESS).build();
90 channels.put(o.getCode(), channel);
91 newChannels.add(channel);
93 if (Objects.equals(defaultLabel, thing.getLabel())) {
94 customizer.withLabel(newDevice.getName());
96 customizer.withChannels(newChannels);
97 Map<String, String> props = getProperties(Objects.requireNonNull(newDevice));
98 customizer.withProperties(props);
99 updateThing(customizer.build());
100 for (Channel channel : newChannels) {
101 updateState(channel.getUID(), OpenClosedType.OPEN);
104 updateStatus(ThingStatus.ONLINE);
107 return config.enable;
111 protected void closeDevice() throws IOException {
113 EvdevDevice currentDevice = device;
116 if (currentDevice != null) {
117 currentDevice.close();
119 logger.debug("Device {} closed", this);
123 String getInstanceName() {
124 LinuxInputConfiguration c = config;
125 if (c == null || c.path == null) {
132 void handleEventsInThread() throws IOException {
133 try (Selector selector = EvdevDevice.openSelector()) {
135 EvdevDevice currentDevice = device;
136 if (currentDevice == null) {
137 throw new IOException("trying to handle events without an device");
139 SelectionKey evdevReady = currentDevice.register(selector);
141 logger.debug("Grabbing device {}", currentDevice);
142 currentDevice.grab(); // ungrab will happen implicitly at device.close()
145 if (Thread.currentThread().isInterrupted()) {
146 logger.debug("Thread interrupted, exiting");
149 logger.trace("Waiting for event");
150 selector.select(20_000);
151 if (selector.selectedKeys().remove(evdevReady)) {
153 Optional<EvdevDevice.InputEvent> ev = currentDevice.nextEvent();
154 if (!ev.isPresent()) {
157 handleEvent(ev.get());
164 private void handleEvent(EvdevDevice.InputEvent event) {
165 if (event.type() != EvdevLibrary.Type.KEY) {
169 Channel channel = channels.get(event.getCode());
170 if (channel == null) {
171 String msg = "Could not find channel for code {}";
172 if (isInitialized()) {
173 logger.warn(msg, event.getCode());
175 logger.debug(msg, event.getCode());
179 logger.debug("Got event: {}", event);
180 // Documented in README.md
181 int eventValue = event.getValue();
182 switch (eventValue) {
183 case EvdevLibrary.KeyEventValue.DOWN:
184 String keyCode = channel.getUID().getIdWithoutGroup();
185 updateState(keyChannel.getUID(), new StringType(keyCode));
186 updateState(channel.getUID(), OpenClosedType.CLOSED);
187 triggerChannel(keyChannel.getUID(), keyCode);
188 triggerChannel(channel.getUID(), CommonTriggerEvents.PRESSED);
189 updateState(keyChannel.getUID(), new StringType());
191 case EvdevLibrary.KeyEventValue.UP:
192 updateState(channel.getUID(), OpenClosedType.OPEN);
193 triggerChannel(channel.getUID(), CommonTriggerEvents.RELEASED);
195 case EvdevLibrary.KeyEventValue.REPEAT:
199 logger.debug("Unexpected event value for channel {}: {}", channel, eventValue);
204 private static Map<String, String> getProperties(EvdevDevice device) {
205 Map<String, String> properties = new HashMap<>();
206 properties.put("physicalLocation", device.getPhys());
207 properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getUniq());
208 properties.put(Thing.PROPERTY_MODEL_ID, hex(device.getProdutId()));
209 properties.put(Thing.PROPERTY_VENDOR, hex(device.getVendorId()));
210 properties.put("busType", device.getBusType().map(Object::toString).orElseGet(() -> hex(device.getBusId())));
211 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, hex(device.getVersionId()));
212 properties.put("driverVersion", hex(device.getDriverVersion()));
216 private static String hex(int i) {
217 return String.format("%04x", i);