diff --git a/CODEOWNERS b/CODEOWNERS index e95e3a80229..d1e56d4ea75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -74,6 +74,7 @@ /bundles/org.openhab.binding.dwdunwetter/ @limdul79 /bundles/org.openhab.binding.ecobee/ @mhilbush /bundles/org.openhab.binding.ecotouch/ @sibbi77 +/bundles/org.openhab.binding.ekey/ @hmerk /bundles/org.openhab.binding.elerotransmitterstick/ @vbier /bundles/org.openhab.binding.energenie/ @hmerk /bundles/org.openhab.binding.enigma2/ @gdolfen diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index c86eafdcd7a..9e4a4b9d41c 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -356,6 +356,11 @@ org.openhab.binding.ecotouch ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.ekey + ${project.version} + org.openhab.addons.bundles org.openhab.binding.elerotransmitterstick diff --git a/bundles/org.openhab.binding.ekey/NOTICE b/bundles/org.openhab.binding.ekey/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.ekey/README.md b/bundles/org.openhab.binding.ekey/README.md new file mode 100644 index 00000000000..5bb649b4319 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/README.md @@ -0,0 +1,135 @@ +# ekey Binding + +This binding connects to [ekey](https://ekey.net/) converter UDP (CV-LAN) using the RARE/MULTI/HOME protocols. + +## Supported Things + +This binding only supports one thing type: + +| Thing | Thing Type | Description | +|-------------|------------|---------------------------------------------| +| cvlan | Thing | Represents a single ekey converter UDP | + +## Thing Configuration + +The binding uses the following configuration parameters. + +| Parameter | Description | +|-----------|----------------------------------------------------------------| +| ipAddress | IPv4 address of the eKey udp converter. A static IP address is recommended.| +| port | The port as configured during the UDP Converter configuration. e.g. 56000 (Binding default) | +| protocol | Can be RARE, MULTI or HOME depending on what the system supports. Binding defaults to RARE | +| delimiter | The delimiter is also defined on the ekey UDP converter - use the ekey configuration software to determine which delimiter is used or to change it. Binding default is `_` (underscore) | + +## Channels + +| Channel ID | Item Type | Protocol | Description | Possible Values | +|------------|-----------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------| +| action | Number | R/M/H | This indicates whether access was granted (value=0) or denied (value=-1). | 0,-1 (136 and 137 with RARE protocol | +| fingerId | Number | R/M/H | This indicates the finger that was used by a person. | 0-9,-1 | +| fsName | String | M | This returns the 4-character-long name that was specified on the controller for the specific terminals. | | +| fsSerial | Number | R/M/H | This returns the serial number for the specific terminal. | +| inputId | Number | M | This indicates which of the four digital inputs was triggered. Value is number of Input. "-1" indicates that no input was triggered. | 0-4,-1 | +| keyId | Number | M | This indicates which of the four keys was used. See ekey documentation on "keys". | 0-4,-1 | +| relayId | Number | R/H | This indicates which relay has been switched. | 0-3,-1 | +| termId | Number | R | This provides the serial number of the packet source. The source can be a fingerprint terminal or the controller (in case of digital inputs). The Serial number has a length of 13. When using RARE mode, only the trailing 8 digits can be returned. | | +| userId | Number | R/M/H | This indicates which user has been detected on the terminal. The value is the numerical order of the user as it was specified on the controller. | 0-99,-1 | +| userName | String | M | This returns the ten-character-long name of the person that has been recognized on the terminal. The name that is returned must have been previously specified on the controller. | | +| userStatus | Number | M | This indicates the status of the user (-1=undefined, 1=enabled, 0= disabled) | 0,1,-1 | + +## Examples + +example.things + +``` +Thing ekey:cvlan:de3b8db06e "Ekey Udp Converter" @ "Control Panel" [ ipAddress="xxx.xxx.xxx.xxx", port="56000", protocol="RARE", delimiter="_" ] +``` + +rare.items + +``` +Number Action "Last action [MAP(ekey_action.map):%d]" { channel="ekey:cvlan:de3b8db06e:action" } +Number FingerID "User used finger [MAP(ekey_finger.map):%d]" { channel="ekey:cvlan:de3b8db06e:fingerId" } +Number RelayID "Last relay that has been swiched [%d]" { channel="ekey:cvlan:de3b8db06e:relayId" } +Number Serialnumber "Serialnumber [%d]" { channel="ekey:cvlan:de3b8db06e:fsSerial" } +Number TerminalID "Last used terminal [MAP(ekey_terminal.map):%d]" { channel="ekey:cvlan:de3b8db06e:termId" } +Number UserID "Last user that accessed the house was [MAP(ekey_names.map):%d]" { channel="ekey:cvlan:de3b8db06e:userId" } +``` + +multi.items + +``` +Number Action "Last action [MAP(ekey_action.map):%d]" { channel="ekey:cvlan:de3b8db06e:action" } +Number FingerID "User used finger [MAP(ekey_finger.map):%d]" { channel="ekey:cvlan:de3b8db06e:fingerId" } +String FsName "Name of Scanner [%s] { channel="ekey:cvlan:de3b8db06e:fsName" } +Number FsSerial "Serialnumber [%d]" { channel="ekey:cvlan:de3b8db06e:fsSerial" } +Number InputID "Last input that has been triggered [%d]" { channel="ekey:cvlan:de3b8db06e:inputId" } +Number KeyID "Last key that has been used [%d]" { channel="ekey:cvlan:de3b8db06e:keyId" } +Number UserID "Last user that accessed the house was [MAP(ekey_names.map):%d]" { channel="ekey:cvlan:de3b8db06e:userId" } +String UserName " Name of Last user that accessed the house was : [%d]" { channel="ekey:cvlan:de3b8db06e:userName" } +Number UserStatus "Last user that accessed the house was [MAP(ekey_names.map):%d]" { channel="ekey:cvlan:de3b8db06e:userStatus" } +``` + +home.items + +``` +Number Action "Last action [MAP(ekey_action.map):%d]" { channel="ekey:cvlan:de3b8db06e:action" } +Number FingerID "User used finger [MAP(ekey_finger.map):%d]" { channel="ekey:cvlan:de3b8db06e:fingerId" } +Number RelayID "Last relay that has been swiched [%d]" { channel="ekey:cvlan:de3b8db06e:relayId" } +Number Serialnumber "Serialnumber [%d]" { channel="ekey:cvlan:de3b8db06e:fsSerial" } +Number UserID "Last user that accessed the house was [MAP(ekey_names.map):%d]" { channel="ekey:cvlan:de3b8db06e:userId" } +``` + +transform/ekey_finger.map [This is just an example, as there is no strict rule what finger belongs to what number] + +```javascript +0=leftlittle +1=leftring +2=leftmiddle +3=leftindex +4=leftthumb +5=rightthumb +6=rightindex +7=rightmiddle +8=rightring +9=rightlittle +-1=unknown +``` + +transform/ekey_names.map [NO spaces allowed] + +```javascript +-1=Unspecified +1=JohnDoe +2=JaneDoe +``` + +transform/ekey_terminal.map + +```javascript +80156839130911=Front +80156839130914=Back +``` + +transform/ekey_multi_action.map + +```javascript +0=granted +-1=rejected +1=timeoutA +2=timeoutB +3=inactive +4=alwaysuser +5=notcoupled +6=digitalinput +``` + +transform/ekey_rare_action.map + +```javascript +136=granted +137=rejected +``` + + + diff --git a/bundles/org.openhab.binding.ekey/pom.xml b/bundles/org.openhab.binding.ekey/pom.xml new file mode 100644 index 00000000000..c7682fba563 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.binding.ekey + + openHAB Add-ons :: Bundles :: Ekey Binding + + diff --git a/bundles/org.openhab.binding.ekey/src/main/feature/feature.xml b/bundles/org.openhab.binding.ekey/src/main/feature/feature.xml new file mode 100644 index 00000000000..6281b979da5 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.ekey/${project.version} + + diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyBindingConstants.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyBindingConstants.java new file mode 100644 index 00000000000..4d18a926776 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyBindingConstants.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EkeyBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EkeyBindingConstants { + + public static final String BINDING_ID = "ekey"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_CVLAN = new ThingTypeUID(BINDING_ID, "cvlan"); + + // List of all Channel ids + public static final String CHANNEL_TYPE_USERID = "userId"; + public static final String CHANNEL_TYPE_USERNAME = "userName"; + public static final String CHANNEL_TYPE_USERSTATUS = "userStatus"; + public static final String CHANNEL_TYPE_FINGERID = "fingerId"; + public static final String CHANNEL_TYPE_KEYID = "keyId"; + public static final String CHANNEL_TYPE_FSSERIAL = "fsSerial"; + public static final String CHANNEL_TYPE_FSNAME = "fsName"; + public static final String CHANNEL_TYPE_ACTION = "action"; + public static final String CHANNEL_TYPE_INPUTID = "inputId"; + public static final String CHANNEL_TYPE_RELAYID = "relayId"; + public static final String CHANNEL_TYPE_TERMID = "termId"; + public static final String CHANNEL_TYPE_RESERVED = "relayId"; + public static final String CHANNEL_TYPE_EVENT = "event"; + public static final String CHANNEL_TYPE_TIMESTAMP = "timestamp"; +} diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyConfiguration.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyConfiguration.java new file mode 100644 index 00000000000..ae7f029e42b --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link EkeyConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EkeyConfiguration { + + public String ipAddress = ""; + public int port; + public String protocol = ""; + public String delimiter = ""; +} diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyDynamicStateDescriptionProvider.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyDynamicStateDescriptionProvider.java new file mode 100644 index 00000000000..b64f24f8c0a --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyDynamicStateDescriptionProvider.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * + * @author Hans-Jörg Merk - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, EkeyDynamicStateDescriptionProvider.class }) +@NonNullByDefault +public class EkeyDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider { + + @Activate + public EkeyDynamicStateDescriptionProvider(final @Reference EventPublisher eventPublisher, // + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } +} diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyHandlerFactory.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyHandlerFactory.java new file mode 100644 index 00000000000..832a6c0e944 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/EkeyHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal; + +import static org.openhab.binding.ekey.internal.EkeyBindingConstants.THING_TYPE_CVLAN; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ekey.internal.handler.EkeyHandler; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link EkeyHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.ekey", service = ThingHandlerFactory.class) +public class EkeyHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_CVLAN); + + private final EkeyDynamicStateDescriptionProvider ekeyStateDescriptionProvider; + + @Activate + public EkeyHandlerFactory(ComponentContext componentContext, + final @Reference EkeyDynamicStateDescriptionProvider dynamicStateDescriptionProvider) { + this.ekeyStateDescriptionProvider = dynamicStateDescriptionProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_CVLAN.equals(thingTypeUID)) { + return new EkeyHandler(thing, ekeyStateDescriptionProvider); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/api/EkeyPacketListener.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/api/EkeyPacketListener.java new file mode 100644 index 00000000000..1aecf0a8fc3 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/api/EkeyPacketListener.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.ThingStatus; + +/** + * The {@link EkeyPacketListener} is in interface for a Ekey packet received consumer + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public interface EkeyPacketListener { + /** + * This method will be called in case a message was received. + * + */ + void messageReceived(byte[] message); + + /** + * This method will be called in case the connection status has changed. + * + */ + void connectionStatusChanged(ThingStatus status, byte @Nullable [] message); +} diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/api/EkeyUdpPacketReceiver.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/api/EkeyUdpPacketReceiver.java new file mode 100644 index 00000000000..5114ee2f24c --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/api/EkeyUdpPacketReceiver.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal.api; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ekey.internal.handler.EkeyHandler; +import org.openhab.core.thing.ThingStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Class provides the DatagramSocket that listens for eKey packets on the network + * This will run in a thread and can be interrupted by calling stopListener() + * Before starting the thread initialization is required (mode, ip, port and deliminator) + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EkeyUdpPacketReceiver { + + private final Logger logger = LoggerFactory.getLogger(EkeyUdpPacketReceiver.class); + + private final int buffersize = 1024; + private final String ipAddress; + private final int port; + private final String readerThreadName; + + private @Nullable DatagramSocket socket; + + private @Nullable EkeyPacketListener packetListener; + + private boolean connected = false; + + public EkeyUdpPacketReceiver(final String ipAddress, final int port, final String readerThreadName) { + this.ipAddress = ipAddress; + this.port = port; + this.readerThreadName = readerThreadName; + } + + public void openConnection() throws IOException { + closeConnection(); + + EkeyPacketListener listener = this.packetListener; + + socket = new DatagramSocket(port); + + Thread udpListener = new Thread(new UDPListener()); + udpListener.setName(readerThreadName); + udpListener.setDaemon(true); + udpListener.start(); + + setConnected(true); + if (listener != null) { + listener.connectionStatusChanged(ThingStatus.ONLINE, null); + } + } + + public void closeConnection() { + setConnected(false); + try { + DatagramSocket localSocket = socket; + if (localSocket != null) { + localSocket.close(); + localSocket = null; + } + } catch (Exception exception) { + logger.debug("closeConnection(): Error closing connection - {}", exception.getMessage()); + } + } + + private class UDPListener implements Runnable { + + /** + * Run method. Runs the MessageListener thread + */ + @Override + public void run() { + logger.debug("Starting ekey Packet Receiver"); + + DatagramSocket localSocket = socket; + + if (localSocket == null) { + throw new IllegalStateException("Cannot access socket."); + } + + byte[] lastPacket = null; + DatagramPacket packet = new DatagramPacket(new byte[buffersize], buffersize); + packet.setData(new byte[buffersize]); + + while (isConnected()) { + try { + logger.trace("Listen for incoming packet"); + localSocket.receive(packet); + logger.trace("Packet received - {}", packet.getData()); + } catch (IOException exception) { + logger.debug("Exception during packet read - {}", exception.getMessage()); + } + InetAddress sourceIp; + try { + sourceIp = InetAddress.getByName(ipAddress); + if (packet.getAddress().equals(sourceIp)) { + lastPacket = packet.getData(); + readMessage(lastPacket); + } else { + logger.warn("Packet received from unknown source- {}", packet.getData()); + } + } catch (UnknownHostException e) { + logger.debug("Exception during address conversion - {}", e.getMessage()); + } + } + } + } + + public void readMessage(byte[] message) { + EkeyPacketListener listener = this.packetListener; + + if (listener != null && message.length != 0) { + listener.messageReceived(message); + } + } + + public void addEkeyPacketListener(EkeyPacketListener listener) { + if (this.packetListener == null) { + this.packetListener = listener; + } + } + + public void setConnected(boolean connected) { + this.connected = connected; + } + + public boolean isConnected() { + return this.connected; + } + + public void removeEkeyPacketListener(EkeyHandler ekeyHandler) { + if (this.packetListener != null) { + this.packetListener = null; + } + } +} diff --git a/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/handler/EkeyHandler.java b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/handler/EkeyHandler.java new file mode 100644 index 00000000000..7bfc719f1e1 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/java/org/openhab/binding/ekey/internal/handler/EkeyHandler.java @@ -0,0 +1,364 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.ekey.internal.handler; + +import static org.openhab.binding.ekey.internal.EkeyBindingConstants.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ekey.internal.EkeyConfiguration; +import org.openhab.binding.ekey.internal.EkeyDynamicStateDescriptionProvider; +import org.openhab.binding.ekey.internal.api.EkeyPacketListener; +import org.openhab.binding.ekey.internal.api.EkeyUdpPacketReceiver; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EkeyHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Hans-Jörg Merk - Initial contribution + */ +@NonNullByDefault +public class EkeyHandler extends BaseThingHandler implements EkeyPacketListener { + + private final Logger logger = LoggerFactory.getLogger(EkeyHandler.class); + + private final EkeyDynamicStateDescriptionProvider ekeyStateDescriptionProvider; + + private EkeyConfiguration config = new EkeyConfiguration(); + private @Nullable EkeyUdpPacketReceiver receiver; + + public EkeyHandler(Thing thing, EkeyDynamicStateDescriptionProvider ekeyStateDescriptionProvider) { + super(thing); + this.ekeyStateDescriptionProvider = ekeyStateDescriptionProvider; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // The binding does not handle any command + } + + @Override + public void initialize() { + logger.debug("ekey handler initializing"); + config = getConfigAs(EkeyConfiguration.class); + + if (!config.ipAddress.isEmpty() && config.port != 0) { + updateStatus(ThingStatus.UNKNOWN); + + scheduler.submit(() -> { + populateChannels(config.protocol); + String readerThreadName = "OH-binding-" + getThing().getUID().getAsString(); + + EkeyUdpPacketReceiver localReceiver = receiver = new EkeyUdpPacketReceiver(config.ipAddress, + config.port, readerThreadName); + localReceiver.addEkeyPacketListener(this); + try { + localReceiver.openConnection(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Cannot open connection)"); + } + updateStatus(ThingStatus.ONLINE); + }); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address specified)"); + } + } + + @Override + public void dispose() { + EkeyUdpPacketReceiver localReceiver = this.receiver; + + if (localReceiver != null) { + localReceiver.closeConnection(); + localReceiver.removeEkeyPacketListener(this); + } + super.dispose(); + } + + @Override + public void messageReceived(byte[] message) { + logger.debug("messageReceived() : {}", message); + config = getConfigAs(EkeyConfiguration.class); + String delimiter = config.delimiter; + + switch (config.protocol) { + case "RARE": + parseRare(message, delimiter); + break; + case "MULTI": + parseMulti(message, delimiter); + break; + case "HOME": + parseHome(message, delimiter); + break; + } + } + + @Override + public void connectionStatusChanged(ThingStatus status, byte @Nullable [] message) { + if (message != null) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, null); + } + this.updateStatus(status); + } + + public void parseRare(byte[] message, String delimiter) { + logger.debug("parse RARE packet"); + if (message.length >= 72) { + byte[] newMessage = Arrays.copyOf(message, 72); + String messageString = new String(newMessage); + long action = getIntValueFrom(newMessage, 4, 4); + long terminalid = getIntValueFrom(newMessage, 8, 4); + String terminalSerial = getStringValueFrom(newMessage, 12, 14); + long relayid = getIntValueFrom(newMessage, 26, 1); + long userid = getIntValueFrom(newMessage, 28, 4); + long fingerid = getIntValueFrom(newMessage, 32, 4); + int serial = reconstructFsSerial(terminalid); + if (logger.isTraceEnabled()) { + logger.trace("messageString received() : {}", messageString); + logger.trace("AKTION : {}", action); + logger.trace("TERMINAL SERIAL : {}", terminalSerial); + logger.trace("RESERVED : {}", getStringValueFrom(newMessage, 27, 1)); + logger.trace("RELAY ID : {}", relayid); + logger.trace("USER ID : {}", userid); + logger.trace("FINGER ID : {}", fingerid); + logger.trace("EVENT : {}", getStringValueFrom(newMessage, 36, 16)); + logger.trace("FS SERIAL : {}", serial); + } + updateState(CHANNEL_TYPE_ACTION, new DecimalType(action)); + if (!terminalSerial.isEmpty()) { + updateState(CHANNEL_TYPE_TERMID, DecimalType.valueOf(terminalSerial)); + } else { + updateState(CHANNEL_TYPE_TERMID, DecimalType.valueOf("-1")); + } + updateState(CHANNEL_TYPE_RESERVED, StringType.valueOf(getStringValueFrom(newMessage, 27, 1))); + updateState(CHANNEL_TYPE_RELAYID, new DecimalType(relayid)); + updateState(CHANNEL_TYPE_USERID, new DecimalType(userid)); + updateState(CHANNEL_TYPE_FINGERID, new DecimalType(fingerid)); + updateState(CHANNEL_TYPE_EVENT, StringType.valueOf(getStringValueFrom(newMessage, 36, 16))); + updateState(CHANNEL_TYPE_FSSERIAL, new DecimalType(serial)); + + } + } + + public void parseMulti(byte[] message, String delimiter) { + logger.debug("parse MULTI packet"); + if (message.length >= 46) { + byte[] newMessage = Arrays.copyOf(message, 46); + String messageString = new String(newMessage); + String[] array = messageString.split(delimiter); + if (logger.isTraceEnabled()) { + logger.trace("messageString received() : {}", messageString); + logger.trace("USER ID : {}", array[1]); + logger.trace("USER ID : {}", array[1]); + logger.trace("USER NAME : {}", array[2]); + logger.trace("USER STATUS : {}", array[3]); + logger.trace("FINGER ID : {}", array[4]); + logger.trace("KEY ID : {}", array[5]); + logger.trace("SERIENNR FS : {}", array[6]); + logger.trace("NAME FS : {}", new String(array[7]).replace("-", "")); + logger.trace("AKTION : {}", array[8]); + logger.trace("INPUT ID : {}", array[9]); + + } + if (!"-".equals(array[1])) { + updateState(CHANNEL_TYPE_USERID, DecimalType.valueOf((array[1]))); + } else { + updateState(CHANNEL_TYPE_USERID, DecimalType.valueOf("-1")); + } + String userName = (array[2]).toString(); + if (!userName.isEmpty()) { + userName = userName.replace("-", ""); + userName = userName.replace(" ", ""); + updateState(CHANNEL_TYPE_USERNAME, StringType.valueOf(userName)); + } + if (!"-".equals(array[3])) { + updateState(CHANNEL_TYPE_USERSTATUS, DecimalType.valueOf((array[3]))); + } else { + updateState(CHANNEL_TYPE_USERSTATUS, DecimalType.valueOf("-1")); + } + if (!"-".equals(array[4])) { + updateState(CHANNEL_TYPE_FINGERID, DecimalType.valueOf((array[4]))); + } else { + updateState(CHANNEL_TYPE_FINGERID, DecimalType.valueOf("-1")); + } + if (!"-".equals(array[5])) { + updateState(CHANNEL_TYPE_KEYID, DecimalType.valueOf((array[5]))); + } else { + updateState(CHANNEL_TYPE_KEYID, DecimalType.valueOf("-1")); + } + updateState(CHANNEL_TYPE_FSSERIAL, DecimalType.valueOf((array[6]))); + updateState(CHANNEL_TYPE_FSNAME, new StringType(new String(array[7]).replace("-", ""))); + updateState(CHANNEL_TYPE_ACTION, DecimalType.valueOf((array[8]))); + if (!"-".equals(array[9])) { + updateState(CHANNEL_TYPE_INPUTID, DecimalType.valueOf((array[9]))); + } else { + updateState(CHANNEL_TYPE_INPUTID, DecimalType.valueOf("-1")); + } + } else { + logger.trace("received packet is to short : {}", message); + } + } + + public void parseHome(byte[] message, String delimiter) { + logger.debug("parse HOME packet"); + if (message.length >= 27) { + byte[] newMessage = Arrays.copyOf(message, 27); + String messageString = new String(newMessage); + String[] array = messageString.split(delimiter); + if (logger.isTraceEnabled()) { + logger.trace("messageString received() : {}", messageString); + logger.trace("USER ID : {}", array[1]); + logger.trace("FINGER ID : {}", array[2]); + logger.trace("SERIENNR FS : {}", array[3]); + logger.trace("AKTION : {}", array[4]); + logger.trace("RELAY ID : {}", array[5]); + } + if (!"-".equals(array[1])) { + updateState(CHANNEL_TYPE_USERID, DecimalType.valueOf((array[1]))); + } else { + updateState(CHANNEL_TYPE_USERID, DecimalType.valueOf("-1")); + } + if (!"-".equals(array[2])) { + updateState(CHANNEL_TYPE_FINGERID, DecimalType.valueOf((array[2]))); + } else { + updateState(CHANNEL_TYPE_FINGERID, DecimalType.valueOf("-1")); + } + updateState(CHANNEL_TYPE_FSSERIAL, DecimalType.valueOf((array[3]))); + updateState(CHANNEL_TYPE_ACTION, DecimalType.valueOf((array[4]))); + if (!"-".equals(array[5])) { + State relayId = DecimalType.valueOf((array[5])); + updateState(CHANNEL_TYPE_RELAYID, relayId); + } else { + updateState(CHANNEL_TYPE_RELAYID, DecimalType.valueOf("-1")); + } + } else { + logger.trace("received packet is to short : {}", message); + } + } + + public void addChannel(String channelId, String itemType, @Nullable final Collection options) { + if (thing.getChannel(channelId) == null) { + logger.debug("Channel '{}' for UID to be added", channelId); + ThingBuilder thingBuilder = editThing(); + final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId); + Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), itemType) + .withType(channelTypeUID).withKind(ChannelKind.STATE).build(); + thingBuilder.withChannel(channel); + updateThing(thingBuilder.build()); + } + if (options != null) { + final List stateOptions = options.stream() + .map(e -> new StateOption(e, e.substring(0, 1) + e.substring(1).toLowerCase())) + .collect(Collectors.toList()); + logger.debug("StateOptions : '{}'", stateOptions); + ekeyStateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), stateOptions); + } + } + + public void populateChannels(String protocol) { + String channelId = ""; + String itemType = "Number"; + + switch (protocol) { + case "HOME": + channelId = CHANNEL_TYPE_RELAYID; + addChannel(channelId, itemType, null); + break; + case "MULTI": + channelId = CHANNEL_TYPE_USERNAME; + addChannel(channelId, "String", null); + channelId = CHANNEL_TYPE_USERSTATUS; + addChannel(channelId, itemType, null); + channelId = CHANNEL_TYPE_KEYID; + addChannel(channelId, itemType, null); + channelId = CHANNEL_TYPE_FSNAME; + addChannel(channelId, "String", null); + channelId = CHANNEL_TYPE_INPUTID; + addChannel(channelId, itemType, null); + break; + case "RARE": + channelId = CHANNEL_TYPE_TERMID; + addChannel(channelId, itemType, null); + channelId = CHANNEL_TYPE_RELAYID; + addChannel(channelId, itemType, null); + channelId = CHANNEL_TYPE_RESERVED; + addChannel(channelId, "String", null); + channelId = CHANNEL_TYPE_EVENT; + addChannel(channelId, "String", null); + break; + } + } + + private long getIntValueFrom(byte[] bytes, int start, int length) { + if (start + length > bytes.length) { + return -1; + } + long value = 0; + int bits = 0; + for (int i = start; i < start + length; i++) { + value |= (bytes[i] & 0xFF) << bits; + bits += 8; + } + return value; + } + + private String getStringValueFrom(byte[] bytes, int start, int length) { + if (start + length > bytes.length) { + logger.debug("Could not get String from bytes"); + return ""; + } + StringBuffer value = new StringBuffer(); + for (int i = start + length - 1; i >= start; i--) { + if (bytes[i] > (byte) ' ' && bytes[i] < (byte) '~') { + value.append((char) bytes[i]); + } + } + return value.toString(); + } + + private int reconstructFsSerial(long termID) { + long s = termID; + s ^= 0x70000000; + int ssss = (int) (s & 0xFFFF); + s >>= 16; + int yy = (int) s % 53; + int ww = (int) s / 53; + yy *= 1000000; + ww *= 10000; + return ww + yy + ssss; + } +} diff --git a/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..ad2d0f017d4 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Ekey Binding + This is the binding for connecting openHAB to Ekey fingerprint scanners. + + diff --git a/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/i18n/ekey.properties b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/i18n/ekey.properties new file mode 100644 index 00000000000..ee012eb4eeb --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/i18n/ekey.properties @@ -0,0 +1,46 @@ +# binding +binding.ekey.name = Ekey Binding +binding.ekey.description = This Binding integrates Ekey CV-LAN UDP adapters + +# thing types +thing-type.ekey.label = CV-LAN +thing-type.ekey.description = CV-LAN UDP adapter + +# thing type config description +thing-type.config.ekey.ipAddress.label = IP Address +thing-type.config.ekey.ipAddress.description = IPv4 address of the eKey udp converter. A static IP address is recommended. +thing-type.config.ekey.port.label = Port +thing-type.config.ekey.port.description = The port as configured during the UDP Converter configuration. e.g. 56000 +thing-type.config.ekey.protocol.label = Protocol +thing-type.config.ekey.protocol.description = Can be RARE, MULTI or HOME depending on what the system supports +thing-type.config.ekey.delimiter.label = Delimiter +thing-type.config.ekey.delimiter.description = The delimiter is also defined on the ekey UDP converter - use the ekey configuration software to determine which delimiter is used or to change it. Another option is _ (underscore) + +# channel types +channel-type.ekey.userId.label = User ID +channel-type.ekey.userId.description = This indicates which user has been detected on the terminal. The value is the numerical order of the user as it was specified on the controller. +channel-type.ekey.userName.label = Username +channel-type.ekey.userName.description = Returns the ten-character-long name of the person that has been recognized on the terminal +channel-type.ekey.userStatus.label = User status +channel-type.ekey.userStatus.description = Indicates the status of the user (-1=undefined, 1=enabled, 0= disabled) +channel-type.ekey.fingerId.label = Finger ID +channel-type.ekey.fingerId.description = Indicates which finger has been detected on the terminal. (0-9,-1) +channel-type.ekey.keyId.label = Key ID +channel-type.ekey.keyId.description = Indicates which of the four keys was used. See ekey documentation on "keys". +channel-type.ekey.fsSerial.label = Serialnumber +channel-type.ekey.fsSerial.description = This returns the 4-character-long name that was specified on the controller for the specific terminals +channel-type.ekey.fsName.label = Terminal Name +channel-type.ekey.fsName.description = This returns the terminal name +channel-type.ekey.action.label = Action +channel-type.ekey.action.description = Indicates whether access was granted (value=0) or denied (value=-1). According to the ekey documentation there are six more values possible +channel-type.ekey.inputId.label = Input ID +channel-type.ekey.inputId.description = Indicates which of the four digital inputs was triggered. Value is number of Input. "-1" indicates that no input was triggered. +channel-type.ekey.relayId.label = Relay ID +channel-type.ekey.relayId.description = Indicates which relay has been switched. +channel-type.ekey.termId.label = Terminal ID +channel-type.ekey.termId.description = This provides the serial number of the packet source. The source can be a fingerprint terminal or the controller (in case of digital inputs). The Serial number has a length of 13. +channel-type.ekey.reserved.label = Reserved +channel-type.ekey.reserved.description = Reserved +channel-type.ekey.event.label = Event +channel-type.ekey.event.description = Event + diff --git a/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/i18n/ekey_de.properties b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/i18n/ekey_de.properties new file mode 100644 index 00000000000..0e3c029c165 --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/i18n/ekey_de.properties @@ -0,0 +1,46 @@ +# binding +binding.ekey.name = Ekey Binding +binding.ekey.description = Dieses Binding integriert die ekey CV-LAN converter UDP. + +# thing types +thing-type.ekey.label = CV-LAN +thing-type.ekey.description = eKey CV-LAN converter UDP + +# thing type config description +thing-type.config.ekey.ipAddress.label = IP-Adresse +thing-type.config.ekey.ipAddress.description = IPv4 Adresse des eKey converter UDP. Eine statische IPv4 Adresse wird empfohlen. +thing-type.config.ekey.port.label = Empfänger Port +thing-type.config.ekey.port.description = Der während der Konfiguration des ekey converter UDP festgelegte Empfänger Port, z.B. 56000 +thing-type.config.ekey.protocol.label = Protokoll +thing-type.config.ekey.protocol.description = Das vom ekey converter UDP verwendete Protokoll (RARE, MULTI oder HOME, abhängig vom verwendeten System). +thing-type.config.ekey.delimiter.label = Abstandshalter +thing-type.config.ekey.delimiter.description = Der im ekey converter UDP Protokoll verwendete Feldtrenner. + +# channel types +channel-type.ekey.userId.label = Benutzer ID +channel-type.ekey.userId.description = Zeigt an, welcher Benutzer vom Terminal erkannt wurde. Es handelt sich um den im Konverter festgelegten numerischen Wert (0-99). +channel-type.ekey.userName.label = Benutzername +channel-type.ekey.userName.description = Zeigt den 10-stelligen Benutzernamen des vom Terminal erkannten Benutzers. +channel-type.ekey.userStatus.label = Benutzerstatus +channel-type.ekey.userStatus.description = Zeigt den Status der vom Terminal erkannten Benutzers an (-1=nicht definiert, 1=nicht gesperrt, 0=gesperrt) +channel-type.ekey.fingerId.label = Finger ID +channel-type.ekey.fingerId.description = Zeigt den vom Terminal erkannten Finger an. (0-9,-) +channel-type.ekey.keyId.label = Schlüssel ID +channel-type.ekey.keyId.description = Zegt an, welcher der 4 Schlüssel verwendet wurde. Siehe ekey Dokumentation "Schlüssel". +channel-type.ekey.fsSerial.label = Seriennummer +channel-type.ekey.fsSerial.description = Zeiget die Seriennummer des Terminals. +channel-type.ekey.fsName.label = Terminalname +channel-type.ekey.fsName.description = Zeiget den im Kontrolle hinterlegten 4-stelligen Namen des Terminal. +channel-type.ekey.action.label = Aktion +channel-type.ekey.action.description = Zeigt an, ob die letzte Aktion erfolgreich war (Wert=0) oder abgewiesen wurde (Wert=-1). Nach der ekey Dokumentation sind weitere 6 Werte möglich. +channel-type.ekey.inputId.label = Eingangs ID +channel-type.ekey.inputId.description = Zeigt an, welcher der 4 digitalen Eingänge ausgelöst wurde. Der Wert entspricht dem Eingang "-1" bedeutet kein Eingang. +channel-type.ekey.relayId.label = Relais ID +channel-type.ekey.relayId.description = Zeigt an, welches der Relais geschaltet wurde. Der Wert entspricht dem Relais "-1" bedeutet kein Relais +channel-type.ekey.termId.label = Terminal ID +channel-type.ekey.termId.description = Zeigt die Seriennummer des Senders am. Sender kann ein Terminal oder Kontroller sein (Bei digitalen Eingängen). Die Seriennummer ist 13-stellig. +channel-type.ekey.reserved.label = Reserviert +channel-type.ekey.reserved.description = Reserviert +channel-type.ekey.event.label = Ereignis +channel-type.ekey.event.description = Ereignis - Beschreibung nicht verfügbar. + diff --git a/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/thing/dynamic-channels.xml b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/thing/dynamic-channels.xml new file mode 100644 index 00000000000..f19e9d9261a --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/thing/dynamic-channels.xml @@ -0,0 +1,62 @@ + + + + + String + + @text/channel-type.ekey.userName.description + + + + Number + + @text/channel-type.ekey.userStatus.description + + + + Number + + @text/channel-type.ekey.keyId.description + + + + String + + @text/channel-type.ekey.fsName.description + + + + Number + + @text/channel-type.ekey.inputId.description + + + + Number + + @text/channel-type.ekey.relayId.description + + + + Number + + @text/channel-type.ekey.termId.description + + + + String + + @text/channel-type.ekey.reserved.description + + + + String + + @text/channel-type.ekey.event.description + + + + diff --git a/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..04a217258df --- /dev/null +++ b/bundles/org.openhab.binding.ekey/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,71 @@ + + + + + + @text/thing-type.ekey.description + + + + + + + + + + + network-address + + @text/thing-type.config.ekey.ipAddress.description + + + + @text/thing-type.config.ekey.port.description + 56000 + + + + @text/thing-type.config.ekey.protocol.description + RARE + + + + + + + + + @text/thing-type.config.ekey.delimiter.description + _ + + + + + + Number + + @text/channel-type.ekey.userId.description + + + + Number + + @text/channel-type.ekey.fingerId.description + + + + Number + + @text/channel-type.ekey.fsSerial.description + + + + Number + + @text/channel-type.ekey.action.description + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 47e550dd497..2ac8d20aee5 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -106,6 +106,7 @@ org.openhab.binding.dwdunwetter org.openhab.binding.ecobee org.openhab.binding.ecotouch + org.openhab.binding.ekey org.openhab.binding.elerotransmitterstick org.openhab.binding.energenie org.openhab.binding.enigma2