]> git.basschouten.com Git - openhab-addons.git/blob
1648fc594aa2e4207b2387b2c37d302cc341dd85
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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             if (name == null) {
99                 name = Integer.toString(o.getCode());
100             }
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())
104                     .build();
105             channels.put(o.getCode(), channel);
106             newChannels.add(channel);
107         }
108         if (Objects.equals(defaultLabel, thing.getLabel())) {
109             customizer.withLabel(newDevice.getName());
110         }
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);
117         }
118         if (config.enable) {
119             updateStatus(ThingStatus.ONLINE);
120         }
121         device = newDevice;
122         return config.enable;
123     }
124
125     @Override
126     protected void closeDevice() throws IOException {
127         @Nullable
128         EvdevDevice currentDevice = device;
129         device = null;
130
131         if (currentDevice != null) {
132             currentDevice.close();
133         }
134         logger.debug("Device {} closed", this);
135     }
136
137     @Override
138     String getInstanceName() {
139         LinuxInputConfiguration c = config;
140         if (c == null || c.path == null) {
141             return "unknown";
142         }
143         return c.path;
144     }
145
146     @Override
147     void handleEventsInThread() throws IOException {
148         try (Selector selector = EvdevDevice.openSelector()) {
149             @Nullable
150             EvdevDevice currentDevice = device;
151             if (currentDevice == null) {
152                 throw new IOException("trying to handle events without a device");
153             }
154             SelectionKey evdevReady = currentDevice.register(selector);
155
156             logger.debug("Grabbing device {}", currentDevice);
157             currentDevice.grab(); // ungrab will happen implicitly at device.close()
158
159             while (true) {
160                 if (Thread.currentThread().isInterrupted()) {
161                     logger.debug("Thread interrupted, exiting");
162                     break;
163                 }
164                 logger.trace("Waiting for event");
165                 selector.select(20_000);
166                 if (selector.selectedKeys().remove(evdevReady)) {
167                     while (true) {
168                         Optional<EvdevDevice.InputEvent> ev = currentDevice.nextEvent();
169                         if (!ev.isPresent()) {
170                             break;
171                         }
172                         handleEvent(ev.get());
173                     }
174                 }
175             }
176         }
177     }
178
179     private void handleEvent(EvdevDevice.InputEvent event) {
180         if (event.type() != EvdevLibrary.Type.KEY) {
181             return;
182         }
183         @Nullable
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());
189             } else {
190                 logger.debug(msg, event.getCode());
191             }
192             return;
193         }
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());
205                 break;
206             case EvdevLibrary.KeyEventValue.UP:
207                 updateState(channel.getUID(), OpenClosedType.OPEN);
208                 triggerChannel(channel.getUID(), CommonTriggerEvents.RELEASED);
209                 break;
210             case EvdevLibrary.KeyEventValue.REPEAT:
211                 /* Ignored */
212                 break;
213             default:
214                 logger.debug("Unexpected event value for channel {}: {}", channel, eventValue);
215                 break;
216         }
217     }
218
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()));
228         return properties;
229     }
230
231     private static String hex(int i) {
232         return String.format("%04x", i);
233     }
234 }