]> git.basschouten.com Git - openhab-addons.git/blob
7b4e74b8bf218a2ed8a72cbc919f6ba4b1c15162
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.linuxinput.internal;
14
15 import static org.openhab.binding.linuxinput.internal.LinuxInputBindingConstants.*;
16
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;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Optional;
27
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;
46
47 /**
48  * Handler for Linux Input devices.
49  *
50  * @author Thomas Weißschuh - Initial contribution
51  */
52 @NonNullByDefault
53 public final class LinuxInputHandler extends DeviceReadingHandler {
54
55     private final Logger logger = LoggerFactory.getLogger(LinuxInputHandler.class);
56
57     private final Map<Integer, Channel> channels;
58     private final Channel keyChannel;
59     private @Nullable EvdevDevice device;
60     private final @Nullable String defaultLabel;
61
62     private @NonNullByDefault({}) LinuxInputConfiguration config;
63
64     public LinuxInputHandler(Thing thing, @Nullable String defaultLabel) {
65         super(thing);
66         this.defaultLabel = defaultLabel;
67
68         keyChannel = ChannelBuilder.create(new ChannelUID(thing.getUID(), "key"), CoreItemFactory.STRING)
69                 .withType(CHANNEL_TYPE_KEY).build();
70         channels = Collections.synchronizedMap(new HashMap<>());
71     }
72
73     @Override
74     public void handleCommand(ChannelUID channelUID, Command command) {
75         /* no commands to handle */
76     }
77
78     @Override
79     boolean immediateSetup() {
80         config = getConfigAs(LinuxInputConfiguration.class);
81         channels.clear();
82         String statusDesc = null;
83         if (!config.enable) {
84             statusDesc = "Administratively disabled";
85         }
86         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, statusDesc);
87         return true;
88     }
89
90     @Override
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);
103         }
104         if (Objects.equals(defaultLabel, thing.getLabel())) {
105             customizer.withLabel(newDevice.getName());
106         }
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);
113         }
114         if (config.enable) {
115             updateStatus(ThingStatus.ONLINE);
116         }
117         device = newDevice;
118         return config.enable;
119     }
120
121     @Override
122     protected void closeDevice() throws IOException {
123         @Nullable
124         EvdevDevice currentDevice = device;
125         device = null;
126
127         if (currentDevice != null) {
128             currentDevice.close();
129         }
130         logger.debug("Device {} closed", this);
131     }
132
133     @Override
134     String getInstanceName() {
135         LinuxInputConfiguration c = config;
136         if (c == null || c.path == null) {
137             return "unknown";
138         }
139         return c.path;
140     }
141
142     @Override
143     void handleEventsInThread() throws IOException {
144         try (Selector selector = EvdevDevice.openSelector()) {
145             @Nullable
146             EvdevDevice currentDevice = device;
147             if (currentDevice == null) {
148                 throw new IOException("trying to handle events without an device");
149             }
150             SelectionKey evdevReady = currentDevice.register(selector);
151
152             logger.debug("Grabbing device {}", currentDevice);
153             currentDevice.grab(); // ungrab will happen implicitly at device.close()
154
155             while (true) {
156                 if (Thread.currentThread().isInterrupted()) {
157                     logger.debug("Thread interrupted, exiting");
158                     break;
159                 }
160                 logger.trace("Waiting for event");
161                 selector.select(20_000);
162                 if (selector.selectedKeys().remove(evdevReady)) {
163                     while (true) {
164                         Optional<EvdevDevice.InputEvent> ev = currentDevice.nextEvent();
165                         if (!ev.isPresent()) {
166                             break;
167                         }
168                         handleEvent(ev.get());
169                     }
170                 }
171             }
172         }
173     }
174
175     private void handleEvent(EvdevDevice.InputEvent event) {
176         if (event.type() != EvdevLibrary.Type.KEY) {
177             return;
178         }
179         @Nullable
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());
185             } else {
186                 logger.debug(msg, event.getCode());
187             }
188             return;
189         }
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());
201                 break;
202             case EvdevLibrary.KeyEventValue.UP:
203                 updateState(channel.getUID(), OpenClosedType.OPEN);
204                 triggerChannel(channel.getUID(), CommonTriggerEvents.RELEASED);
205                 break;
206             case EvdevLibrary.KeyEventValue.REPEAT:
207                 /* Ignored */
208                 break;
209             default:
210                 logger.debug("Unexpected event value for channel {}: {}", channel, eventValue);
211                 break;
212         }
213     }
214
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()));
224         return properties;
225     }
226
227     private static String hex(int i) {
228         return String.format("%04x", i);
229     }
230 }