2 * Copyright (c) 2010-2023 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;
20 import java.util.concurrent.ConcurrentHashMap;
22 import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingHandler;
23 import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingService;
24 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceListener;
25 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceService;
26 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
27 import org.openhab.core.config.discovery.AbstractDiscoveryService;
28 import org.openhab.core.config.discovery.DiscoveryResult;
29 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
30 import org.openhab.core.config.discovery.DiscoveryService;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingUID;
33 import org.osgi.service.component.annotations.Component;
34 import org.pcap4j.core.PcapNetworkInterface;
35 import org.pcap4j.util.MacAddress;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link AmazonDashButtonDiscoveryService} is responsible for discovering Amazon Dash Buttons. It does so by
41 * capturing ARP and BOOTP requests from all available network devices.
43 * While scanning the user has to press the button in order to send an ARP and BOOTP request packet. The
44 * {@link AmazonDashButtonDiscoveryService} captures this packet and checks the device's MAC address which sent the
45 * request against a static list of vendor prefixes ({@link #VENDOR_PREFIXES}).
47 * If an Amazon MAC address is detected a {@link DiscoveryResult} is built and passed to
48 * {@link #thingDiscovered(DiscoveryResult)}.
50 * @author Oliver Libutzki - Initial contribution
53 @Component(service = DiscoveryService.class, configurationPid = "discovery.amazondashbutton")
54 public class AmazonDashButtonDiscoveryService extends AbstractDiscoveryService implements PcapNetworkInterfaceListener {
56 private static final int DISCOVER_TIMEOUT_SECONDS = 30;
58 private final Logger logger = LoggerFactory.getLogger(AmazonDashButtonDiscoveryService.class);
61 * The Amazon Dash button vendor prefixes
64 private static final Set<String> VENDOR_PREFIXES = Set.of(
94 * Returns true if the passed macAddress is an Amazon MAC address.
99 private static boolean isAmazonVendor(String macAddress) {
100 String vendorPrefix = macAddress.substring(0, 8).toUpperCase();
101 return VENDOR_PREFIXES.contains(vendorPrefix);
104 private final Map<PcapNetworkInterfaceWrapper, PacketCapturingService> packetCapturingServices = new ConcurrentHashMap<>();
106 private boolean explicitScanning = false;
107 private boolean backgroundScanning = false;
109 public AmazonDashButtonDiscoveryService() {
110 super(Set.of(DASH_BUTTON_THING_TYPE), DISCOVER_TIMEOUT_SECONDS, false);
114 protected void startScan() {
115 explicitScanning = true;
116 updateListenerRegistry();
120 protected synchronized void stopScan() {
121 explicitScanning = false;
122 updateListenerRegistry();
127 protected void startBackgroundDiscovery() {
128 backgroundScanning = true;
129 updateListenerRegistry();
133 protected void stopBackgroundDiscovery() {
134 backgroundScanning = false;
135 updateListenerRegistry();
139 public void onPcapNetworkInterfaceAdded(final PcapNetworkInterfaceWrapper networkInterface) {
140 startCapturing(networkInterface);
144 public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper networkInterface) {
145 stopCapturing(networkInterface);
148 private void updateListenerRegistry() {
149 boolean shouldListen = explicitScanning || backgroundScanning;
151 PcapNetworkInterfaceService.instance().registerListener(this);
152 // Start capturing for all network interfaces
153 final Set<PcapNetworkInterfaceWrapper> networkInterfaces = PcapNetworkInterfaceService.instance()
154 .getNetworkInterfaces();
155 for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
156 startCapturing(pcapNetworkInterface);
159 PcapNetworkInterfaceService.instance().unregisterListener(this);
160 // Stop capturing for all network interfaces
161 final Set<PcapNetworkInterfaceWrapper> networkInterfaces = packetCapturingServices.keySet();
162 for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
163 stopCapturing(pcapNetworkInterface);
169 * Stops capturing for packets for the given {@link PcapNetworkInterface}.
171 * @param pcapNetworkInterface The {@link PcapNetworkInterface} the capturing should be stopped for.
173 private void stopCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
174 final PacketCapturingService packetCapturingService = packetCapturingServices.remove(pcapNetworkInterface);
175 final String interfaceName = pcapNetworkInterface.getName();
176 if (packetCapturingService != null) {
177 packetCapturingService.stopCapturing();
178 logger.debug("Stopped capturing for {}.", interfaceName);
180 logger.warn("No active PacketCapturingService registered for {}.", interfaceName);
185 * Starts capturing for packets for the given {@link PcapNetworkInterface}. If the network interface is already
186 * captured this method returns without doing anything.
188 * @param pcapNetworkInterface The {@link PcapNetworkInterface} to be captured
190 private void startCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
191 if (packetCapturingServices.containsKey(pcapNetworkInterface)) {
192 // We already have a tracker
196 PacketCapturingService packetCapturingService = new PacketCapturingService(pcapNetworkInterface);
198 packetCapturingServices.put(pcapNetworkInterface, packetCapturingService);
199 final String interfaceName = pcapNetworkInterface.getName();
200 final boolean capturingStarted = packetCapturingService.startCapturing(new PacketCapturingHandler() {
203 public void packetCaptured(MacAddress macAddress) {
204 String macAdressString = macAddress.toString();
206 if (isAmazonVendor(macAdressString)) {
207 logger.debug("Captured a packet from {} which seems to be sent from an Amazon Dash Button device.",
209 ThingUID dashButtonThing = new ThingUID(DASH_BUTTON_THING_TYPE, macAdressString.replace(":", "-"));
211 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(dashButtonThing)
212 .withLabel("Dash Button")
213 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS)
214 .withProperty(Thing.PROPERTY_MAC_ADDRESS, macAdressString)
215 .withProperty(PROPERTY_NETWORK_INTERFACE_NAME, interfaceName)
216 .withProperty(PROPERTY_PACKET_INTERVAL, BigDecimal.valueOf(5000))
219 thingDiscovered(discoveryResult);
222 "Captured a packet from {} which is ignored as it's not on the list of supported vendor prefixes.",
227 if (capturingStarted) {
228 logger.debug("Started capturing for {}.", interfaceName);