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