2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.amazondashbutton.internal.discovery;
15 import static org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.Collections;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.stream.Collectors;
23 import java.util.stream.Stream;
25 import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingHandler;
26 import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingService;
27 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceListener;
28 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceService;
29 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
30 import org.openhab.core.config.discovery.AbstractDiscoveryService;
31 import org.openhab.core.config.discovery.DiscoveryResult;
32 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
33 import org.openhab.core.config.discovery.DiscoveryService;
34 import org.openhab.core.thing.ThingUID;
35 import org.osgi.service.component.annotations.Component;
36 import org.pcap4j.core.PcapNetworkInterface;
37 import org.pcap4j.util.MacAddress;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * The {@link AmazonDashButtonDiscoveryService} is responsible for discovering Amazon Dash Buttons. It does so by
43 * capturing ARP and BOOTP requests from all available network devices.
45 * While scanning the user has to press the button in order to send an ARP and BOOTP request packet. The
46 * {@link AmazonDashButtonDiscoveryService} captures this packet and checks the device's MAC address which sent the
47 * request against a static list of vendor prefixes ({@link #VENDOR_PREFIXES}).
49 * If an Amazon MAC address is detected a {@link DiscoveryResult} is built and passed to
50 * {@link #thingDiscovered(DiscoveryResult)}.
52 * @author Oliver Libutzki - Initial contribution
55 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.amazondashbutton")
56 public class AmazonDashButtonDiscoveryService extends AbstractDiscoveryService implements PcapNetworkInterfaceListener {
58 private static final int DISCOVER_TIMEOUT_SECONDS = 30;
60 private final Logger logger = LoggerFactory.getLogger(AmazonDashButtonDiscoveryService.class);
63 * The Amazon Dash button vendor prefixes
66 private static final Set<String> VENDOR_PREFIXES = Collections.unmodifiableSet(Stream.of(
92 ).collect(Collectors.toSet()));
96 * Returns true if the passed macAddress is an Amazon MAC address.
101 private static boolean isAmazonVendor(String macAddress) {
102 String vendorPrefix = macAddress.substring(0, 8).toUpperCase();
103 return VENDOR_PREFIXES.contains(vendorPrefix);
106 private final Map<PcapNetworkInterfaceWrapper, PacketCapturingService> packetCapturingServices = new ConcurrentHashMap<>();
108 private boolean explicitScanning = false;
109 private boolean backgroundScanning = false;
111 public AmazonDashButtonDiscoveryService() {
112 super(Collections.singleton(DASH_BUTTON_THING_TYPE), DISCOVER_TIMEOUT_SECONDS, false);
116 protected void startScan() {
117 explicitScanning = true;
118 updateListenerRegistry();
122 protected synchronized void stopScan() {
123 explicitScanning = false;
124 updateListenerRegistry();
129 protected void startBackgroundDiscovery() {
130 backgroundScanning = true;
131 updateListenerRegistry();
135 protected void stopBackgroundDiscovery() {
136 backgroundScanning = false;
137 updateListenerRegistry();
141 public void onPcapNetworkInterfaceAdded(final PcapNetworkInterfaceWrapper networkInterface) {
142 startCapturing(networkInterface);
146 public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper networkInterface) {
147 stopCapturing(networkInterface);
150 private void updateListenerRegistry() {
151 boolean shouldListen = explicitScanning || backgroundScanning;
153 PcapNetworkInterfaceService.instance().registerListener(this);
154 // Start capturing for all network interfaces
155 final Set<PcapNetworkInterfaceWrapper> networkInterfaces = PcapNetworkInterfaceService.instance()
156 .getNetworkInterfaces();
157 for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
158 startCapturing(pcapNetworkInterface);
161 PcapNetworkInterfaceService.instance().unregisterListener(this);
162 // Stop capturing for all network interfaces
163 final Set<PcapNetworkInterfaceWrapper> networkInterfaces = packetCapturingServices.keySet();
164 for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
165 stopCapturing(pcapNetworkInterface);
171 * Stops capturing for packets for the given {@link PcapNetworkInterface}.
173 * @param pcapNetworkInterface The {@link PcapNetworkInterface} the capturing should be stopped for.
175 private void stopCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
176 final PacketCapturingService packetCapturingService = packetCapturingServices.remove(pcapNetworkInterface);
177 final String interfaceName = pcapNetworkInterface.getName();
178 if (packetCapturingService != null) {
179 packetCapturingService.stopCapturing();
180 logger.debug("Stopped capturing for {}.", interfaceName);
182 logger.warn("No active PacketCapturingService registered for {}.", interfaceName);
187 * Starts capturing for packets for the given {@link PcapNetworkInterface}. If the network interface is already
188 * captured this method returns without doing anything.
190 * @param pcapNetworkInterface The {@link PcapNetworkInterface} to be captured
192 private void startCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
193 if (packetCapturingServices.containsKey(pcapNetworkInterface)) {
194 // We already have a tracker
198 PacketCapturingService packetCapturingService = new PacketCapturingService(pcapNetworkInterface);
200 packetCapturingServices.put(pcapNetworkInterface, packetCapturingService);
201 final String interfaceName = pcapNetworkInterface.getName();
202 final boolean capturingStarted = packetCapturingService.startCapturing(new PacketCapturingHandler() {
205 public void packetCaptured(MacAddress macAddress) {
206 String macAdressString = macAddress.toString();
208 if (isAmazonVendor(macAdressString)) {
209 logger.debug("Captured a packet from {} which seems to be sent from an Amazon Dash Button device.",
211 ThingUID dashButtonThing = new ThingUID(DASH_BUTTON_THING_TYPE, macAdressString.replace(":", "-"));
213 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(dashButtonThing)
214 .withLabel("Dash Button")
215 .withRepresentationProperty(macAdressString)
216 .withProperty(PROPERTY_MAC_ADDRESS, macAdressString)
217 .withProperty(PROPERTY_NETWORK_INTERFACE_NAME, interfaceName)
218 .withProperty(PROPERTY_PACKET_INTERVAL, BigDecimal.valueOf(5000))
221 thingDiscovered(discoveryResult);
224 "Captured a packet from {} which is ignored as it's not on the list of supported vendor prefixes.",
229 if (capturingStarted) {
230 logger.debug("Started capturing for {}.", interfaceName);