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;
18 import java.util.Collections;
21 import java.util.concurrent.ConcurrentHashMap;
23 import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingHandler;
24 import org.openhab.binding.amazondashbutton.internal.capturing.PacketCapturingService;
25 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceListener;
26 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceService;
27 import org.openhab.binding.amazondashbutton.internal.pcap.PcapNetworkInterfaceWrapper;
28 import org.openhab.core.config.discovery.AbstractDiscoveryService;
29 import org.openhab.core.config.discovery.DiscoveryResult;
30 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
31 import org.openhab.core.config.discovery.DiscoveryService;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingUID;
34 import org.osgi.service.component.annotations.Component;
35 import org.pcap4j.core.PcapNetworkInterface;
36 import org.pcap4j.util.MacAddress;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * The {@link AmazonDashButtonDiscoveryService} is responsible for discovering Amazon Dash Buttons. It does so by
42 * capturing ARP and BOOTP requests from all available network devices.
44 * While scanning the user has to press the button in order to send an ARP and BOOTP request packet. The
45 * {@link AmazonDashButtonDiscoveryService} captures this packet and checks the device's MAC address which sent the
46 * request against a static list of vendor prefixes ({@link #VENDOR_PREFIXES}).
48 * If an Amazon MAC address is detected a {@link DiscoveryResult} is built and passed to
49 * {@link #thingDiscovered(DiscoveryResult)}.
51 * @author Oliver Libutzki - Initial contribution
54 @Component(service = DiscoveryService.class, configurationPid = "discovery.amazondashbutton")
55 public class AmazonDashButtonDiscoveryService extends AbstractDiscoveryService implements PcapNetworkInterfaceListener {
57 private static final int DISCOVER_TIMEOUT_SECONDS = 30;
59 private final Logger logger = LoggerFactory.getLogger(AmazonDashButtonDiscoveryService.class);
62 * The Amazon Dash button vendor prefixes
65 private static final Set<String> VENDOR_PREFIXES = Set.of(
95 * Returns true if the passed macAddress is an Amazon MAC address.
100 private static boolean isAmazonVendor(String macAddress) {
101 String vendorPrefix = macAddress.substring(0, 8).toUpperCase();
102 return VENDOR_PREFIXES.contains(vendorPrefix);
105 private final Map<PcapNetworkInterfaceWrapper, PacketCapturingService> packetCapturingServices = new ConcurrentHashMap<>();
107 private boolean explicitScanning = false;
108 private boolean backgroundScanning = false;
110 public AmazonDashButtonDiscoveryService() {
111 super(Collections.singleton(DASH_BUTTON_THING_TYPE), DISCOVER_TIMEOUT_SECONDS, false);
115 protected void startScan() {
116 explicitScanning = true;
117 updateListenerRegistry();
121 protected synchronized void stopScan() {
122 explicitScanning = false;
123 updateListenerRegistry();
128 protected void startBackgroundDiscovery() {
129 backgroundScanning = true;
130 updateListenerRegistry();
134 protected void stopBackgroundDiscovery() {
135 backgroundScanning = false;
136 updateListenerRegistry();
140 public void onPcapNetworkInterfaceAdded(final PcapNetworkInterfaceWrapper networkInterface) {
141 startCapturing(networkInterface);
145 public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper networkInterface) {
146 stopCapturing(networkInterface);
149 private void updateListenerRegistry() {
150 boolean shouldListen = explicitScanning || backgroundScanning;
152 PcapNetworkInterfaceService.instance().registerListener(this);
153 // Start capturing for all network interfaces
154 final Set<PcapNetworkInterfaceWrapper> networkInterfaces = PcapNetworkInterfaceService.instance()
155 .getNetworkInterfaces();
156 for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
157 startCapturing(pcapNetworkInterface);
160 PcapNetworkInterfaceService.instance().unregisterListener(this);
161 // Stop capturing for all network interfaces
162 final Set<PcapNetworkInterfaceWrapper> networkInterfaces = packetCapturingServices.keySet();
163 for (PcapNetworkInterfaceWrapper pcapNetworkInterface : networkInterfaces) {
164 stopCapturing(pcapNetworkInterface);
170 * Stops capturing for packets for the given {@link PcapNetworkInterface}.
172 * @param pcapNetworkInterface The {@link PcapNetworkInterface} the capturing should be stopped for.
174 private void stopCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
175 final PacketCapturingService packetCapturingService = packetCapturingServices.remove(pcapNetworkInterface);
176 final String interfaceName = pcapNetworkInterface.getName();
177 if (packetCapturingService != null) {
178 packetCapturingService.stopCapturing();
179 logger.debug("Stopped capturing for {}.", interfaceName);
181 logger.warn("No active PacketCapturingService registered for {}.", interfaceName);
186 * Starts capturing for packets for the given {@link PcapNetworkInterface}. If the network interface is already
187 * captured this method returns without doing anything.
189 * @param pcapNetworkInterface The {@link PcapNetworkInterface} to be captured
191 private void startCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
192 if (packetCapturingServices.containsKey(pcapNetworkInterface)) {
193 // We already have a tracker
197 PacketCapturingService packetCapturingService = new PacketCapturingService(pcapNetworkInterface);
199 packetCapturingServices.put(pcapNetworkInterface, packetCapturingService);
200 final String interfaceName = pcapNetworkInterface.getName();
201 final boolean capturingStarted = packetCapturingService.startCapturing(new PacketCapturingHandler() {
204 public void packetCaptured(MacAddress macAddress) {
205 String macAdressString = macAddress.toString();
207 if (isAmazonVendor(macAdressString)) {
208 logger.debug("Captured a packet from {} which seems to be sent from an Amazon Dash Button device.",
210 ThingUID dashButtonThing = new ThingUID(DASH_BUTTON_THING_TYPE, macAdressString.replace(":", "-"));
212 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(dashButtonThing)
213 .withLabel("Dash Button")
214 .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS)
215 .withProperty(Thing.PROPERTY_MAC_ADDRESS, macAdressString)
216 .withProperty(PROPERTY_NETWORK_INTERFACE_NAME, interfaceName)
217 .withProperty(PROPERTY_PACKET_INTERVAL, BigDecimal.valueOf(5000))
220 thingDiscovered(discoveryResult);
223 "Captured a packet from {} which is ignored as it's not on the list of supported vendor prefixes.",
228 if (capturingStarted) {
229 logger.debug("Started capturing for {}.", interfaceName);