added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.amazondashbutton-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-amazondashbutton" description="Amazon Dash Button Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-runtime-jna</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.amazondashbutton/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AmazonDashButtonBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Oliver Libutzki - Initial contribution
*/
@NonNullByDefault
public class AmazonDashButtonBindingConstants {
public static final String BINDING_ID = "amazondashbutton";
// List of all Thing Type UIDs
public static final ThingTypeUID DASH_BUTTON_THING_TYPE = new ThingTypeUID(BINDING_ID, "dashbutton");
// List of all Channel ids
public static final String PRESS = "press";
// Custom Properties
public static final String PROPERTY_MAC_ADDRESS = "macAddress";
public static final String PROPERTY_NETWORK_INTERFACE_NAME = "pcapNetworkInterfaceName";
public static final String PROPERTY_PACKET_INTERVAL = "packetInterval";
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal;
import static org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants.DASH_BUTTON_THING_TYPE;
import java.util.Collections;
import java.util.Set;
import org.openhab.binding.amazondashbutton.internal.handler.AmazonDashButtonHandler;
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.annotations.Component;
/**
* The {@link AmazonDashButtonHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Oliver Libutzki - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.amazondashbutton")
public class AmazonDashButtonHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(DASH_BUTTON_THING_TYPE);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(DASH_BUTTON_THING_TYPE)) {
return new AmazonDashButtonHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.capturing;
import org.pcap4j.util.MacAddress;
/**
* The {@link PacketCapturingHandler} is notified if a packet is captured by {@link PacketCapturingService}.
*
* @author Oliver Libutzki - Initial contribution
*
*/
public interface PacketCapturingHandler {
/**
* Callback method to handle a captured packet.
*
* @param macAddress The mac address which sent the packet
*/
public void packetCaptured(MacAddress sourceMacAddress);
}

View File

@@ -0,0 +1,184 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.capturing;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
import org.pcap4j.core.BpfProgram.BpfCompileMode;
import org.pcap4j.core.NotOpenException;
import org.pcap4j.core.PacketListener;
import org.pcap4j.core.PcapHandle;
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode;
import org.pcap4j.packet.ArpPacket;
import org.pcap4j.packet.EthernetPacket;
import org.pcap4j.packet.Packet;
import org.pcap4j.packet.UdpPacket;
import org.pcap4j.packet.namednumber.ArpOperation;
import org.pcap4j.packet.namednumber.UdpPort;
import org.pcap4j.util.MacAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PacketCapturingService} is responsible for capturing packets.
*
* @author Oliver Libutzki - Initial contribution
*
*/
public class PacketCapturingService {
private final Logger logger = LoggerFactory.getLogger(PacketCapturingService.class);
private static final int READ_TIMEOUT = 10; // [ms]
private static final int SNAPLEN = 65536; // [bytes]
private final PcapNetworkInterfaceWrapper pcapNetworkInterface;
private PcapHandle pcapHandle;
public PacketCapturingService(PcapNetworkInterfaceWrapper pcapNetworkInterface) {
this.pcapNetworkInterface = pcapNetworkInterface;
}
/**
* Calls {@link #startCapturing(PacketCapturingHandler, String)} with a null MAC address.
*
* @param packetCapturingHandler The handler to be called every time packet is captured
* @return Returns true, if the capturing has been started successfully, otherwise returns false
*/
public boolean startCapturing(final PacketCapturingHandler packetCapturingHandler) {
return startCapturing(packetCapturingHandler, null);
}
/**
* Starts the capturing in a dedicated thread, so this method returns immediately. Every time a packet is captured,
* the {@link PacketCapturingHandler#packetCaptured(MacAddress)} of the given
* {@link PacketCapturingHandler} is called.
*
* It's possible to capture packets sent by a specific MAC address by providing the given parameter. If the
* macAddress is null, all MAC addresses are considered.
*
* @param packetCapturingHandler The handler to be called every time a packet is captured
* @param macAddress The source MAC address of the captured packet, might be null in order to deactivate this filter
* criteria
* @return Returns true, if the capturing has been started successfully, otherwise returns false
* @throws IllegalStateException Thrown if {@link PcapHandle#isOpen()} of {@link #pcapHandle} returns true
*/
public boolean startCapturing(final PacketCapturingHandler packetCapturingHandler, final String macAddress) {
if (pcapHandle != null) {
if (pcapHandle.isOpen()) {
throw new IllegalStateException("There is an open pcap handle.");
} else {
pcapHandle.close();
}
}
try {
pcapHandle = pcapNetworkInterface.openLive(SNAPLEN, PromiscuousMode.PROMISCUOUS, READ_TIMEOUT);
StringBuilder filterBuilder = new StringBuilder("(arp or port bootps)");
if (macAddress != null) {
filterBuilder.append(" and ether src " + macAddress);
}
pcapHandle.setFilter(filterBuilder.toString(), BpfCompileMode.OPTIMIZE);
} catch (Exception e) {
logger.error("Capturing packets on device {} failed.", pcapNetworkInterface.getName(), e);
return false;
}
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
try {
pcapHandle.loop(-1, new PacketListener() {
@Override
public void gotPacket(Packet packet) {
if (!packet.contains(EthernetPacket.class)) {
return;
}
final EthernetPacket ethernetPacket = packet.get(EthernetPacket.class);
final MacAddress sourceMacAddress = ethernetPacket.getHeader().getSrcAddr();
if (shouldCapture(packet)) {
packetCapturingHandler.packetCaptured(sourceMacAddress);
}
}
});
} finally {
if (pcapHandle != null && pcapHandle.isOpen()) {
pcapHandle.close();
pcapHandle = null;
}
}
return null;
});
if (macAddress == null) {
logger.debug("Started capturing ARP and BOOTP requests for network device {}.",
pcapNetworkInterface.getName());
} else {
logger.debug("Started capturing ARP and BOOTP requests for network device {} and MAC address {}.",
pcapNetworkInterface.getName(), macAddress);
}
return true;
}
/**
* Checks if the given {@link Packet} should be captured.
*
* @param packet The packet to be checked
* @return Returns true, if the packet should be captured, otherwise false
*/
private boolean shouldCapture(final Packet packet) {
if (packet.contains(ArpPacket.class)) {
ArpPacket arpPacket = packet.get(ArpPacket.class);
if (arpPacket.getHeader().getOperation().equals(ArpOperation.REQUEST)) {
return true;
}
}
if (packet.contains(UdpPacket.class)) {
final UdpPacket udpPacket = packet.get(UdpPacket.class);
if (UdpPort.BOOTPS == udpPacket.getHeader().getDstPort()) {
return true;
}
}
return false;
}
/**
* Stops the capturing. This can be called without calling {@link #startCapturing(PacketCapturingHandler)} or
* {@link #startCapturing(PacketCapturingHandler, String)} before.
*/
public void stopCapturing() {
if (pcapHandle != null) {
if (pcapHandle.isOpen()) {
try {
pcapHandle.breakLoop();
logger.debug("Stopped capturing ARP and BOOTP requests for network device {}.",
pcapNetworkInterface.getName());
} catch (NotOpenException e) {
// Just ignore
}
} else {
pcapHandle = null;
}
}
}
/**
* Returns the tracked {@link PcapNetworkInterfaceWrapper}.
*
* @return the tracked {@link PcapNetworkInterfaceWrapper}
*/
public PcapNetworkInterfaceWrapper getPcapNetworkInterface() {
return pcapNetworkInterface;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.config;
/**
* The configuration of the Amazon Dash Button
*
* @author Oliver Libutzki - Initial contribution
*
*/
public class AmazonDashButtonConfig {
/**
* The MAC address of the Amazon Dash Button
*/
public String macAddress;
/**
* The network interface which receives the packets of the Amazon Dash Button
*/
public String pcapNetworkInterfaceName;
/**
* Often a single button press is recognized multiple times. You can specify how long any further detected button
* pressed should be ignored after one click is handled (in ms).
*/
public Integer packetInterval;
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.config;
import static org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceService;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
import org.openhab.core.config.core.ConfigOptionProvider;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.thing.ThingTypeUID;
import org.osgi.service.component.annotations.Component;
import org.pcap4j.core.PcapAddress;
/**
* The {@link AmazonDashButtonConfigOptionProvider} is responsible for providing options for the
* {@link AmazonDashButtonBindingConstants#PROPERTY_NETWORK_INTERFACE_NAME} property.
*
* @author Oliver Libutzki - Initial contribution
*
*/
@Component(service = ConfigOptionProvider.class)
@NonNullByDefault
public class AmazonDashButtonConfigOptionProvider implements ConfigOptionProvider {
@Override
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable Locale locale) {
return getParameterOptions(uri, param, null, locale);
}
@Override
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
@Nullable Locale locale) {
if ("thing-type".equals(uri.getScheme())) {
ThingTypeUID thingtypeUID = new ThingTypeUID(uri.getSchemeSpecificPart());
if (thingtypeUID.equals(DASH_BUTTON_THING_TYPE) && PROPERTY_NETWORK_INTERFACE_NAME.equals(param)) {
return getPcapNetworkInterfacesOptions();
}
}
return null;
}
private Collection<ParameterOption> getPcapNetworkInterfacesOptions() {
Set<PcapNetworkInterfaceWrapper> pcapNetworkInterfaces = PcapNetworkInterfaceService.instance()
.getNetworkInterfaces();
List<ParameterOption> options = new ArrayList<>();
for (PcapNetworkInterfaceWrapper pcapNetworkInterface : pcapNetworkInterfaces) {
String name = pcapNetworkInterface.getName();
options.add(new ParameterOption(name, getLabel(pcapNetworkInterface)));
}
return options;
}
private String getLabel(PcapNetworkInterfaceWrapper pcapNetworkInterface) {
StringBuilder sb = new StringBuilder(pcapNetworkInterface.getName());
List<PcapAddress> addresses = pcapNetworkInterface.getAddresses();
final String description = pcapNetworkInterface.getDescription();
Set<String> paramStrings = new LinkedHashSet<>();
if (description != null && !description.isEmpty()) {
paramStrings.add(description);
}
for (PcapAddress address : addresses) {
paramStrings.add(address.getAddress().toString().substring(1));
}
boolean hasParams = !paramStrings.isEmpty();
if (hasParams) {
sb.append(" (");
}
for (Iterator<String> paramIterator = paramStrings.iterator(); paramIterator.hasNext();) {
String addressString = paramIterator.next();
sb.append(addressString);
if (paramIterator.hasNext()) {
sb.append(", ");
}
}
if (hasParams) {
sb.append(")");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,233 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.discovery;
import static org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingHandler;
import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingService;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceListener;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceService;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.util.MacAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AmazonDashButtonDiscoveryService} is responsible for discovering Amazon Dash Buttons. It does so by
* capturing ARP and BOOTP requests from all available network devices.
*
* While scanning the user has to press the button in order to send an ARP and BOOTP request packet. The
* {@link AmazonDashButtonDiscoveryService} captures this packet and checks the device's MAC address which sent the
* request against a static list of vendor prefixes ({@link #VENDOR_PREFIXES}).
*
* If an Amazon MAC address is detected a {@link DiscoveryResult} is built and passed to
* {@link #thingDiscovered(DiscoveryResult)}.
*
* @author Oliver Libutzki - Initial contribution
*
*/
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazondashbutton")
public class AmazonDashButtonDiscoveryService extends AbstractDiscoveryService implements PcapNetworkInterfaceListener {
private static final int DISCOVER_TIMEOUT_SECONDS = 30;
private final Logger logger = LoggerFactory.getLogger(AmazonDashButtonDiscoveryService.class);
/**
* The Amazon Dash button vendor prefixes
*/
// @formatter:off
private static final Set<String> VENDOR_PREFIXES = Collections.unmodifiableSet(Stream.of(
"F0:D2:F1",
"88:71:E5",
"FC:A1:83",
"F0:27:2D",
"74:C2:46",
"68:37:E9",
"78:E1:03",
"38:F7:3D",
"50:DC:E7",
"A0:02:DC",
"0C:47:C9",
"74:75:48",
"AC:63:BE",
"FC:A6:67",
"18:74:2E",
"00:FC:8B",
"FC:65:DE",
"6C:56:97",
"44:65:0D",
"50:F5:DA",
"68:54:FD",
"40:B4:CD",
"84:D6:D0",
"34:D2:70",
"B4:7C:9C"
).collect(Collectors.toSet()));
// @formatter:on
/**
* Returns true if the passed macAddress is an Amazon MAC address.
*
* @param macAddress
* @return
*/
private static boolean isAmazonVendor(String macAddress) {
String vendorPrefix = macAddress.substring(0, 8).toUpperCase();
return VENDOR_PREFIXES.contains(vendorPrefix);
}
private final Map<PcapNetworkInterfaceWrapper, PacketCapturingService> packetCapturingServices = new ConcurrentHashMap<>();
private boolean explicitScanning = false;
private boolean backgroundScanning = false;
public AmazonDashButtonDiscoveryService() {
super(Collections.singleton(DASH_BUTTON_THING_TYPE), DISCOVER_TIMEOUT_SECONDS, false);
}
@Override
protected void startScan() {
explicitScanning = true;
updateListenerRegistry();
}
@Override
protected synchronized void stopScan() {
explicitScanning = false;
updateListenerRegistry();
super.stopScan();
}
@Override
protected void startBackgroundDiscovery() {
backgroundScanning = true;
updateListenerRegistry();
}
@Override
protected void stopBackgroundDiscovery() {
backgroundScanning = false;
updateListenerRegistry();
}
@Override
public void onPcapNetworkInterfaceAdded(final PcapNetworkInterfaceWrapper networkInterface) {
startCapturing(networkInterface);
}
@Override
public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper networkInterface) {
stopCapturing(networkInterface);
}
private void updateListenerRegistry() {
boolean shouldListen = explicitScanning || backgroundScanning;
if (shouldListen) {
PcapNetworkInterfaceService.instance().registerListener(this);
// Start capturing for all network interfaces
final Set<PcapNetworkInterfaceWrapper> networkInterfaces = PcapNetworkInterfaceService.instance()
.getNetworkInterfaces();
for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
startCapturing(pcapNetworkInterface);
}
} else {
PcapNetworkInterfaceService.instance().unregisterListener(this);
// Stop capturing for all network interfaces
final Set<PcapNetworkInterfaceWrapper> networkInterfaces = packetCapturingServices.keySet();
for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
stopCapturing(pcapNetworkInterface);
}
}
}
/**
* Stops capturing for packets for the given {@link PcapNetworkInterface}.
*
* @param pcapNetworkInterface The {@link PcapNetworkInterface} the capturing should be stopped for.
*/
private void stopCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
final PacketCapturingService packetCapturingService = packetCapturingServices.remove(pcapNetworkInterface);
final String interfaceName = pcapNetworkInterface.getName();
if (packetCapturingService != null) {
packetCapturingService.stopCapturing();
logger.debug("Stopped capturing for {}.", interfaceName);
} else {
logger.warn("No active PacketCapturingService registered for {}.", interfaceName);
}
}
/**
* Starts capturing for packets for the given {@link PcapNetworkInterface}. If the network interface is already
* captured this method returns without doing anything.
*
* @param pcapNetworkInterface The {@link PcapNetworkInterface} to be captured
*/
private void startCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
if (packetCapturingServices.containsKey(pcapNetworkInterface)) {
// We already have a tracker
return;
}
PacketCapturingService packetCapturingService = new PacketCapturingService(pcapNetworkInterface);
packetCapturingServices.put(pcapNetworkInterface, packetCapturingService);
final String interfaceName = pcapNetworkInterface.getName();
final boolean capturingStarted = packetCapturingService.startCapturing(new PacketCapturingHandler() {
@Override
public void packetCaptured(MacAddress macAddress) {
String macAdressString = macAddress.toString();
if (isAmazonVendor(macAdressString)) {
logger.debug("Captured a packet from {} which seems to be sent from an Amazon Dash Button device.",
macAdressString);
ThingUID dashButtonThing = new ThingUID(DASH_BUTTON_THING_TYPE, macAdressString.replace(":", "-"));
// @formatter:off
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(dashButtonThing)
.withLabel("Dash Button")
.withRepresentationProperty(macAdressString)
.withProperty(PROPERTY_MAC_ADDRESS, macAdressString)
.withProperty(PROPERTY_NETWORK_INTERFACE_NAME, interfaceName)
.withProperty(PROPERTY_PACKET_INTERVAL, BigDecimal.valueOf(5000))
.build();
// @formatter:on
thingDiscovered(discoveryResult);
} else {
logger.trace(
"Captured a packet from {} which is ignored as it's not on the list of supported vendor prefixes.",
macAdressString);
}
}
});
if (capturingStarted) {
logger.debug("Started capturing for {}.", interfaceName);
}
}
}

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.handler;
import static org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants.PRESS;
import static org.openhab.core.thing.CommonTriggerEvents.PRESSED;
import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingHandler;
import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingService;
import org.openhab.binding.amazondashbutton.internal.config.AmazonDashButtonConfig;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceListener;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceService;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
import org.openhab.binding.amazondashbutton.internal.pcap.PcapUtil;
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.types.Command;
import org.pcap4j.util.MacAddress;
/**
* The {@link AmazonDashButtonHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Oliver Libutzki - Initial contribution
*/
public class AmazonDashButtonHandler extends BaseThingHandler implements PcapNetworkInterfaceListener {
private PacketCapturingService packetCapturingService;
private long lastCommandHandled = 0;
public AmazonDashButtonHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// There are no commands to be handled
}
@Override
public void initialize() {
PcapNetworkInterfaceService.instance().registerListener(this);
AmazonDashButtonConfig dashButtonConfig = getConfigAs(AmazonDashButtonConfig.class);
final String pcapNetworkInterfaceName = dashButtonConfig.pcapNetworkInterfaceName;
final String macAddress = dashButtonConfig.macAddress;
final Integer packetInterval = dashButtonConfig.packetInterval;
scheduler.submit(() -> {
PcapNetworkInterfaceWrapper pcapNetworkInterface = PcapUtil
.getNetworkInterfaceByName(pcapNetworkInterfaceName);
if (pcapNetworkInterface == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
"The networkinterface " + pcapNetworkInterfaceName + " is not present.");
return;
}
packetCapturingService = new PacketCapturingService(pcapNetworkInterface);
boolean capturingStarted = packetCapturingService.startCapturing(new PacketCapturingHandler() {
@Override
public void packetCaptured(MacAddress macAddress) {
long now = System.currentTimeMillis();
if (lastCommandHandled + packetInterval < now) {
triggerChannel(PRESS, PRESSED);
lastCommandHandled = now;
}
}
}, macAddress);
if (capturingStarted) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"The capturing for " + pcapNetworkInterfaceName + " cannot be started.");
}
});
}
@Override
public void dispose() {
super.dispose();
if (packetCapturingService != null) {
packetCapturingService.stopCapturing();
packetCapturingService = null;
}
PcapNetworkInterfaceService.instance().unregisterListener(this);
}
@Override
public void onPcapNetworkInterfaceAdded(PcapNetworkInterfaceWrapper newNetworkInterface) {
if (packetCapturingService != null) {
final PcapNetworkInterfaceWrapper trackedPcapNetworkInterface = packetCapturingService
.getPcapNetworkInterface();
if (trackedPcapNetworkInterface.equals(newNetworkInterface)) {
updateStatus(ThingStatus.ONLINE);
}
}
}
@Override
public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper removedNetworkInterface) {
if (packetCapturingService != null) {
final PcapNetworkInterfaceWrapper trackedPcapNetworkInterface = packetCapturingService
.getPcapNetworkInterface();
if (trackedPcapNetworkInterface.equals(removedNetworkInterface)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"The networkinterface " + removedNetworkInterface.getName() + " is not present anymore.");
}
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.pcap;
import org.pcap4j.core.PcapNetworkInterface;
/**
* The {@link PcapNetworkInterfaceListener} is notified whenever a {@link PcapNetworkInterface} is added or removed. A
* {@link PcapNetworkInterfaceListener} can be registered by calling
* {@link PcapNetworkInterfaceService#registerListener(PcapNetworkInterfaceListener)} and can be unregistered by calling
* {@link PcapNetworkInterfaceService#unregisterListener(PcapNetworkInterfaceListener)}.
*
* @author Oliver Libutzki - Initial contribution
*
*/
public interface PcapNetworkInterfaceListener {
/**
* This method is called whenever a new {@link PcapNetworkInterfaceWrapper} is added.
*
* @param newNetworkInterface The added networkInterface
*/
public void onPcapNetworkInterfaceAdded(PcapNetworkInterfaceWrapper newNetworkInterface);
/**
* This method is called whenever a {@link PcapNetworkInterfaceWrapper} is removed.
*
* @param removedNetworkInterface The removed networkInterface
*/
public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper removedNetworkInterface);
}

View File

@@ -0,0 +1,191 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.pcap;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.openhab.core.common.ThreadPoolManager;
import org.pcap4j.core.PcapNetworkInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PcapNetworkInterfaceService} is a singleton which can be obtained by calling {@link #instance()}.
* It provides all available {@link PcapNetworkInterface}s which are bound to an address. These network interfaces can
* be retrieved by calling {@link #getNetworkInterfaces()}.
*
* Moreover the {@link PcapNetworkInterfaceService} provided the possibility to register a
* {@link PcapNetworkInterfaceListener}s which are notified on new and removed {@link PcapNetworkInterface}s.
*
* @author Oliver Libutzki - Initial contribution
*
*/
public class PcapNetworkInterfaceService {
private final Logger logger = LoggerFactory.getLogger(PcapNetworkInterfaceService.class);
private static PcapNetworkInterfaceService instance = null;
private static final String THREADPOOL_NAME = "pcapNetworkInterfaceService";
private final Set<PcapNetworkInterfaceListener> listeners = new CopyOnWriteArraySet<>();
private final Set<PcapNetworkInterfaceWrapper> pcapNetworkInterfaces = new CopyOnWriteArraySet<>();
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THREADPOOL_NAME);
private ScheduledFuture<?> future = null;
private final Runnable pollingRunnable = () -> {
synchronized (pcapNetworkInterfaces) {
final Set<PcapNetworkInterfaceWrapper> determinedNetworkInterfaces = determineBoundNetworkInterfaces();
final Set<PcapNetworkInterfaceWrapper> currentNetworkInterfaces = new HashSet<>(pcapNetworkInterfaces);
final Set<PcapNetworkInterfaceWrapper> newNetworkInterfaces = new HashSet<>(determinedNetworkInterfaces);
newNetworkInterfaces.removeIf(currentNetworkInterfaces::contains);
final Set<PcapNetworkInterfaceWrapper> removedNetworkInterfaces = new HashSet<>(currentNetworkInterfaces);
removedNetworkInterfaces.removeIf(determinedNetworkInterfaces::contains);
pcapNetworkInterfaces.clear();
pcapNetworkInterfaces.addAll(determinedNetworkInterfaces);
for (PcapNetworkInterfaceWrapper pcapNetworkInterface : newNetworkInterfaces) {
notifyNetworkInterfacesAdded(pcapNetworkInterface);
}
for (PcapNetworkInterfaceWrapper pcapNetworkInterface : removedNetworkInterfaces) {
notifyNetworkInterfacesRemoved(pcapNetworkInterface);
}
}
};
private PcapNetworkInterfaceService() {
}
/**
* Returns the {@link PcapNetworkInterfaceService} singleton instance.
*
* @return The {@link PcapNetworkInterfaceService} singleton
*/
public static synchronized PcapNetworkInterfaceService instance() {
if (instance == null) {
instance = new PcapNetworkInterfaceService();
}
return instance;
}
/**
* Returns a {@link Set} of {@link PcapNetworkInterface}s which are bound to an address.
*
* @return the network interface set
*/
public Set<PcapNetworkInterfaceWrapper> getNetworkInterfaces() {
synchronized (pcapNetworkInterfaces) {
return Collections.unmodifiableSet(pcapNetworkInterfaces.stream().collect(Collectors.toSet()));
}
}
/**
* Registers the given {@link PcapNetworkInterfaceListener}. If it is already registered, this method returns
* immediately.
*
* @param networkInterfaceListener The {@link PcapNetworkInterfaceListener} to be registered.
*/
public void registerListener(PcapNetworkInterfaceListener networkInterfaceListener) {
final boolean isAdded = listeners.add(networkInterfaceListener);
if (isAdded) {
updatePollingState();
}
}
/**
* Unregisters the given {@link PcapNetworkInterfaceListener}. If it is already unregistered, this method returns
* immediately.
*
* @param networkInterfaceListener The {@link PcapNetworkInterfaceListener} to be unregistered.
*/
public void unregisterListener(PcapNetworkInterfaceListener networkInterfaceListener) {
final boolean isRemoved = listeners.remove(networkInterfaceListener);
if (isRemoved) {
updatePollingState();
}
}
private void notifyNetworkInterfacesAdded(PcapNetworkInterfaceWrapper pcapNetworkInterface) {
for (PcapNetworkInterfaceListener listener : listeners) {
notifyNetworkInterfacesAdded(listener, pcapNetworkInterface);
}
}
private void notifyNetworkInterfacesRemoved(PcapNetworkInterfaceWrapper pcapNetworkInterface) {
for (PcapNetworkInterfaceListener listener : listeners) {
notifyNetworkInterfacesRemoved(listener, pcapNetworkInterface);
}
}
private void notifyNetworkInterfacesAdded(PcapNetworkInterfaceListener listener,
PcapNetworkInterfaceWrapper pcapNetworkInterface) {
try {
listener.onPcapNetworkInterfaceAdded(pcapNetworkInterface);
} catch (Exception e) {
logger.error("An exception occurred while calling onPcapNetworkInterfaceAdded for {}", listener, e);
}
}
private void notifyNetworkInterfacesRemoved(PcapNetworkInterfaceListener listener,
PcapNetworkInterfaceWrapper pcapNetworkInterface) {
try {
listener.onPcapNetworkInterfaceRemoved(pcapNetworkInterface);
} catch (Exception e) {
logger.error("An exception occurred while calling onPcapNetworkInterfaceRemoved for {}", listener, e);
}
}
/**
* Returns all pcap network interfaces relying on {@link PcapUtil#getAllNetworkInterfaces()}. The list is filtered
* as all interfaces which are not bound to an address are excluded.
*
* @return An {@link Iterable} of all {@link PcapNetworkInterfaceWrapper}s which are bound to an address
*/
private Set<PcapNetworkInterfaceWrapper> determineBoundNetworkInterfaces() {
final Set<PcapNetworkInterfaceWrapper> allNetworkInterfaces = new HashSet<>();
PcapUtil.getAllNetworkInterfaces().forEach(allNetworkInterfaces::add);
allNetworkInterfaces.removeIf(networkInterface -> {
final boolean notSuitable = networkInterface.getAddresses().isEmpty();
if (notSuitable) {
logger.debug("{} is not a suitable network interfaces as no addresses are bound to it.",
networkInterface.getName());
}
return notSuitable;
});
return allNetworkInterfaces;
}
private void updatePollingState() {
boolean isPolling = future != null;
if (isPolling && listeners.isEmpty()) {
future.cancel(true);
future = null;
return;
}
if (!isPolling && !listeners.isEmpty()) {
future = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 2, TimeUnit.SECONDS);
}
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.pcap;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import org.pcap4j.core.PcapAddress;
import org.pcap4j.core.PcapHandle;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.PcapNetworkInterface.PromiscuousMode;
/**
* This wrapper is needed as {@link PcapNetworkInterface#equals(Object)} and {@link PcapNetworkInterface#hashCode()} are
* not implemented in an appropriate way. The wrapper delegates all methods calls except {@link #equals(Object)} and
* {@link #hashCode()} to {@link #pcapNetworkInterface}.
*
* @author Oliver Libutzki - Initial contribution
*
*/
public class PcapNetworkInterfaceWrapper {
/**
* The wrapped object
*/
private final PcapNetworkInterface pcapNetworkInterface;
/**
* Use this Guava function in order to create a {@link PcapNetworkInterfaceWrapper} instance.
*/
public static final Function<PcapNetworkInterface, PcapNetworkInterfaceWrapper> TRANSFORMER = new Function<PcapNetworkInterface, PcapNetworkInterfaceWrapper>() {
@Override
public PcapNetworkInterfaceWrapper apply(PcapNetworkInterface pcapNetworkInterface) {
return new PcapNetworkInterfaceWrapper(pcapNetworkInterface);
}
};
private PcapNetworkInterfaceWrapper(PcapNetworkInterface pcapNetworkInterface) {
if (pcapNetworkInterface == null) {
throw new IllegalArgumentException("Don't pass null.");
}
this.pcapNetworkInterface = pcapNetworkInterface;
}
public List<PcapAddress> getAddresses() {
return pcapNetworkInterface.getAddresses();
}
public String getName() {
return pcapNetworkInterface.getName();
}
public String getDescription() {
return pcapNetworkInterface.getDescription();
}
public PcapHandle openLive(int arg0, PromiscuousMode arg1, int arg2) throws PcapNativeException {
return pcapNetworkInterface.openLive(arg0, arg1, arg2);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PcapNetworkInterfaceWrapper other = (PcapNetworkInterfaceWrapper) obj;
return Objects.equals(this.getName(), other.getName());
}
@Override
public int hashCode() {
return Objects.hashCode(this.getName());
}
@Override
public String toString() {
return pcapNetworkInterface.toString();
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.amazondashbutton.internal.pcap;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.Pcaps;
/**
*
* A simple utitlity class which encapsulates {@link Pcaps} and which catches checked exceptions and transforms them
* into {@link RuntimeException}s.
*
* @author Oliver Libutzki - Initial contribution
*
*/
public class PcapUtil {
/**
* Returns all Pcap network interfaces relying on {@link Pcaps#findAllDevs()}.
*
* @return A {@link Set} of all {@link PcapNetworkInterfaceWrapper}s
*/
public static Set<PcapNetworkInterfaceWrapper> getAllNetworkInterfaces() {
try {
final Set<PcapNetworkInterfaceWrapper> allNetworkInterfaces = Collections.unmodifiableSet(Pcaps
.findAllDevs().stream().map(PcapNetworkInterfaceWrapper.TRANSFORMER).collect(Collectors.toSet()));
return allNetworkInterfaces;
} catch (PcapNativeException e) {
throw new RuntimeException(e);
}
}
/**
* Returns the Pcap network interface with the given name relying on {@link Pcaps#getDevByName(String)}. If no
* interface is found, null is returned.
*
* @param name The name of the Pcap network interface
* @return The network interface with the given name. Returns null, if no interface is found
*/
public static PcapNetworkInterfaceWrapper getNetworkInterfaceByName(String name) {
try {
return PcapNetworkInterfaceWrapper.TRANSFORMER.apply(Pcaps.getDevByName(name));
} catch (PcapNativeException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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
*/
@Requirement(namespace = ExtenderNamespace.EXTENDER_NAMESPACE, filter = "(osgi.extender=osgi.serviceloader.registrar)")
@Requirement(namespace = ExtenderNamespace.EXTENDER_NAMESPACE, filter = "(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0)(!(version>=2.0)))")
@Requirement(namespace = "osgi.serviceloader", filter = "(osgi.serviceloader=org.pcap4j.packet.factory.PacketFactoryBinderProvider)", cardinality = Cardinality.MULTIPLE)
@Capability(namespace = "osgi.serviceloader", name = "org.pcap4j.packet.factory.PacketFactoryBinderProvider")
package org.openhab.binding.amazondashbutton;
import org.osgi.annotation.bundle.Capability;
import org.osgi.annotation.bundle.Requirement;
import org.osgi.annotation.bundle.Requirement.Cardinality;
import org.osgi.namespace.extender.ExtenderNamespace;
/**
* Additional information for AmazonDashButton package
*
* @author Jan N. Klug - Initial contribution
*
*/

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="amazondashbutton" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Amazon Dash Button Binding</name>
<description>@text/bindingDescription</description>
<author>Oliver Libutzki</author>
</binding:binding>

View File

@@ -0,0 +1,15 @@
# binding
bindingDescription = This is the binding for the Amazon Dash Button.
# thing types
dashButtonLabel = Amazon Dash Button
dashButtonDescription = This is the Amazon Dash Button
dashButtonMacAddressLabel = MAC address
dashButtonMacAddressDescription = The MAC address of the Amazon Dash Button
dashButtonNetworkInterfaceLabel = Network interface
dashButtonNetworkInterfaceDescription = The network interface which receives the packets of the Amazon Dash Button
dashButtonPacketIntervalLabel = Packet processing interval (in ms)
dashButtonPacketIntervalDescription = Often a single button press is recognized multiple times. You can specify how long any further detected button pressed should be ignored after one click is handled (in ms).
dashButtonPressChannelLabel = Amazon Dash Button press
dashButtonPressChannelDescription = Channel for recognizing presses on the Amazon Dash Button

View File

@@ -0,0 +1,15 @@
# binding
bindingDescription = Dies ist das Binding für den Amazon Dash Button.
# thing types
dashButtonLabel = Amazon Dash Button
dashButtonDescription = Dies ist der Amazon Dash Button
dashButtonMacAddressLabel = MAC-Adresse
dashButtonMacAddressDescription = Die MAC-Adresse des Amazon Dash Buttons
dashButtonNetworkInterfaceLabel = Netzwerkschnittstelle
dashButtonNetworkInterfaceDescription = Die Netzwerkschnittstelle, über den das Paket des Amazon Dash Button empfangen wird
dashButtonPacketIntervalLabel = Paketverarbeitungsintervall (in ms)
dashButtonPacketIntervalDescription = Häufig führt eine einzelne Button-Betätigung dazu, dass diese mehrfach verarbeitet wird. Es kann angegeben werden, für welchen Zeitraum weitere Betätigungsevents ignoriert werden sollen, nachdem eine Betätigung verarbeitet wurde (in ms).
dashButtonPressChannelLabel = Eine Betätigung des Amazon Dash Button
dashButtonPressChannelDescription = Channel um eine Betätigung des Amazon Dash Button festzustellen

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="amazondashbutton"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="dashbutton">
<label>@text/dashButtonLabel</label>
<description>@text/dashButtonDescription</description>
<channels>
<channel id="press" typeId="system.rawbutton">
<label>@text/dashButtonPressChannelLabel</label>
<description>@text/dashButtonPressChannelLabel</description>
</channel>
</channels>
<config-description>
<parameter name="pcapNetworkInterfaceName" type="text">
<label>@text/dashButtonNetworkInterfaceLabel</label>
<description>@text/dashButtonNetworkInterfaceDescription</description>
</parameter>
<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>@text/dashButtonMacAddressLabel</label>
<description>@text/dashButtonMacAddressDescription</description>
</parameter>
<parameter name="packetInterval" type="integer">
<label>@text/dashButtonPacketIntervalLabel</label>
<description>@text/dashButtonPacketIntervalDescription</description>
<default>5000</default>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>