]> git.basschouten.com Git - openhab-addons.git/blob
626e5fca1838577850928089977e6f9b00a20a68
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.amazondashbutton.internal.discovery;
14
15 import static org.openhab.binding.amazondashbutton.internal.AmazonDashButtonBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.Collections;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.concurrent.ConcurrentHashMap;
22
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;
39
40 /**
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.
43  *
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}).
47  *
48  * If an Amazon MAC address is detected a {@link DiscoveryResult} is built and passed to
49  * {@link #thingDiscovered(DiscoveryResult)}.
50  *
51  * @author Oliver Libutzki - Initial contribution
52  *
53  */
54 @Component(service = DiscoveryService.class, configurationPid = "discovery.amazondashbutton")
55 public class AmazonDashButtonDiscoveryService extends AbstractDiscoveryService implements PcapNetworkInterfaceListener {
56
57     private static final int DISCOVER_TIMEOUT_SECONDS = 30;
58
59     private final Logger logger = LoggerFactory.getLogger(AmazonDashButtonDiscoveryService.class);
60
61     /**
62      * The Amazon Dash button vendor prefixes
63      */
64     // @formatter:off
65     private static final Set<String> VENDOR_PREFIXES = Set.of(
66             "F0:D2:F1",
67             "88:71:E5",
68             "FC:A1:83",
69             "F0:27:2D",
70             "74:C2:46",
71             "68:37:E9",
72             "78:E1:03",
73             "38:F7:3D",
74             "50:DC:E7",
75             "A0:02:DC",
76             "0C:47:C9",
77             "74:75:48",
78             "AC:63:BE",
79             "FC:A6:67",
80             "18:74:2E",
81             "00:FC:8B",
82             "FC:65:DE",
83             "6C:56:97",
84             "44:65:0D",
85             "50:F5:DA",
86             "68:54:FD",
87             "40:B4:CD",
88             "84:D6:D0",
89             "34:D2:70",
90             "B4:7C:9C"
91         );
92     // @formatter:on
93
94     /**
95      * Returns true if the passed macAddress is an Amazon MAC address.
96      *
97      * @param macAddress
98      * @return
99      */
100     private static boolean isAmazonVendor(String macAddress) {
101         String vendorPrefix = macAddress.substring(0, 8).toUpperCase();
102         return VENDOR_PREFIXES.contains(vendorPrefix);
103     }
104
105     private final Map<PcapNetworkInterfaceWrapper, PacketCapturingService> packetCapturingServices = new ConcurrentHashMap<>();
106
107     private boolean explicitScanning = false;
108     private boolean backgroundScanning = false;
109
110     public AmazonDashButtonDiscoveryService() {
111         super(Collections.singleton(DASH_BUTTON_THING_TYPE), DISCOVER_TIMEOUT_SECONDS, false);
112     }
113
114     @Override
115     protected void startScan() {
116         explicitScanning = true;
117         updateListenerRegistry();
118     }
119
120     @Override
121     protected synchronized void stopScan() {
122         explicitScanning = false;
123         updateListenerRegistry();
124         super.stopScan();
125     }
126
127     @Override
128     protected void startBackgroundDiscovery() {
129         backgroundScanning = true;
130         updateListenerRegistry();
131     }
132
133     @Override
134     protected void stopBackgroundDiscovery() {
135         backgroundScanning = false;
136         updateListenerRegistry();
137     }
138
139     @Override
140     public void onPcapNetworkInterfaceAdded(final PcapNetworkInterfaceWrapper networkInterface) {
141         startCapturing(networkInterface);
142     }
143
144     @Override
145     public void onPcapNetworkInterfaceRemoved(PcapNetworkInterfaceWrapper networkInterface) {
146         stopCapturing(networkInterface);
147     }
148
149     private void updateListenerRegistry() {
150         boolean shouldListen = explicitScanning || backgroundScanning;
151         if (shouldListen) {
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);
158             }
159         } else {
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);
165             }
166         }
167     }
168
169     /**
170      * Stops capturing for packets for the given {@link PcapNetworkInterface}.
171      *
172      * @param pcapNetworkInterface The {@link PcapNetworkInterface} the capturing should be stopped for.
173      */
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);
180         } else {
181             logger.warn("No active PacketCapturingService registered for {}.", interfaceName);
182         }
183     }
184
185     /**
186      * Starts capturing for packets for the given {@link PcapNetworkInterface}. If the network interface is already
187      * captured this method returns without doing anything.
188      *
189      * @param pcapNetworkInterface The {@link PcapNetworkInterface} to be captured
190      */
191     private void startCapturing(final PcapNetworkInterfaceWrapper pcapNetworkInterface) {
192         if (packetCapturingServices.containsKey(pcapNetworkInterface)) {
193             // We already have a tracker
194             return;
195         }
196
197         PacketCapturingService packetCapturingService = new PacketCapturingService(pcapNetworkInterface);
198
199         packetCapturingServices.put(pcapNetworkInterface, packetCapturingService);
200         final String interfaceName = pcapNetworkInterface.getName();
201         final boolean capturingStarted = packetCapturingService.startCapturing(new PacketCapturingHandler() {
202
203             @Override
204             public void packetCaptured(MacAddress macAddress) {
205                 String macAdressString = macAddress.toString();
206
207                 if (isAmazonVendor(macAdressString)) {
208                     logger.debug("Captured a packet from {} which seems to be sent from an Amazon Dash Button device.",
209                             macAdressString);
210                     ThingUID dashButtonThing = new ThingUID(DASH_BUTTON_THING_TYPE, macAdressString.replace(":", "-"));
211                     // @formatter:off
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))
218                             .build();
219                     // @formatter:on
220                     thingDiscovered(discoveryResult);
221                 } else {
222                     logger.trace(
223                             "Captured a packet from {} which is ignored as it's not on the list of supported vendor prefixes.",
224                             macAdressString);
225                 }
226             }
227         });
228         if (capturingStarted) {
229             logger.debug("Started capturing for {}.", interfaceName);
230         }
231     }
232 }