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