]> git.basschouten.com Git - openhab-addons.git/blob
5c55f18e76e5e77a0e01e553e539222bc3c47dc4
[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.feican.internal;
14
15 import static org.openhab.binding.feican.internal.FeicanBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.SocketException;
23 import java.net.SocketTimeoutException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.Map;
26 import java.util.TreeMap;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.config.discovery.AbstractDiscoveryService;
31 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
32 import org.openhab.core.config.discovery.DiscoveryService;
33 import org.openhab.core.thing.ThingUID;
34 import org.osgi.service.component.annotations.Component;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * Discovery service for Feican Bulbs. When sending a discovery UDP broadcast command on port 5000 to a Feican bulb. The
40  * bulp will respond with its mac address send via UDP broadcast over port 6000.
41  *
42  * @author Hilbrand Bouwkamp - Initial contribution
43  */
44 @NonNullByDefault
45 @Component(service = DiscoveryService.class, configurationPid = "discovery.feican")
46 public class FeicanDiscoveryService extends AbstractDiscoveryService {
47
48     private static final int DISCOVERY_TIMEOUT_SECONDS = 5;
49     private static final int RECEIVE_JOB_TIMEOUT = 20000;
50     private static final int UDP_PACKET_TIMEOUT = RECEIVE_JOB_TIMEOUT - 50;
51     private static final String FEICAN_NAME_PREFIX = "FC_";
52
53     private final Logger logger = LoggerFactory.getLogger(FeicanDiscoveryService.class);
54
55     ///// Network
56     private final byte[] buffer = new byte[32];
57     @Nullable
58     private DatagramSocket discoverSocket;
59
60     public FeicanDiscoveryService() {
61         super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS, false);
62     }
63
64     @Override
65     protected void startScan() {
66         logger.debug("Start scan for Feican devices.");
67         discoverThings();
68     }
69
70     @Override
71     protected void stopScan() {
72         logger.debug("Stop scan for Feican devices.");
73         closeDiscoverSocket();
74         super.stopScan();
75     }
76
77     /**
78      * Performs the actual discovery of Feican devices (things).
79      */
80     private void discoverThings() {
81         try {
82             final DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
83             // No need to call close first, because the caller of this method already has done it.
84             startDiscoverSocket();
85             // Runs until the socket call gets a time out and throws an exception. When a time out is triggered it means
86             // no data was present and nothing new to discover.
87             while (true) {
88                 // Set packet length in case a previous call reduced the size.
89                 receivePacket.setLength(buffer.length);
90                 if (discoverSocket == null) {
91                     break;
92                 } else {
93                     discoverSocket.receive(receivePacket);
94                 }
95                 logger.debug("Feican device discovery returned package with length {}", receivePacket.getLength());
96                 if (receivePacket.getLength() > 0) {
97                     thingDiscovered(receivePacket);
98                 }
99             }
100         } catch (SocketTimeoutException e) {
101             logger.debug("Discovering poller timeout...");
102         } catch (IOException e) {
103             logger.debug("Error during discovery: {}", e.getMessage());
104         } finally {
105             closeDiscoverSocket();
106             removeOlderResults(getTimestampOfLastScan());
107         }
108     }
109
110     /**
111      * Opens a {@link DatagramSocket} and sends a packet for discovery of Feican devices.
112      *
113      * @throws SocketException
114      * @throws IOException
115      */
116     private void startDiscoverSocket() throws SocketException, IOException {
117         discoverSocket = new DatagramSocket(new InetSocketAddress(Connection.FEICAN_RECEIVE_PORT));
118         discoverSocket.setBroadcast(true);
119         discoverSocket.setSoTimeout(UDP_PACKET_TIMEOUT);
120         final InetAddress broadcast = InetAddress.getByName("255.255.255.255");
121         final DatagramPacket discoverPacket = new DatagramPacket(Commands.discover(), Commands.discover().length,
122                 broadcast, Connection.FEICAN_SEND_PORT);
123         discoverSocket.send(discoverPacket);
124         if (logger.isTraceEnabled()) {
125             logger.trace("Discovery package sent: {}", new String(discoverPacket.getData(), StandardCharsets.UTF_8));
126         }
127     }
128
129     /**
130      * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a
131      * synchronized context.
132      */
133     private void closeDiscoverSocket() {
134         if (discoverSocket != null) {
135             discoverSocket.close();
136             discoverSocket = null;
137         }
138     }
139
140     /**
141      * Register a device (thing) with the discovered properties.
142      *
143      * @param packet containing data of detected device
144      */
145     private void thingDiscovered(DatagramPacket packet) {
146         final String ipAddress = packet.getAddress().getHostAddress();
147         if (packet.getData().length < 12) {
148             logger.debug(
149                     "Feican device was detected, but the retrieved data is incomplete: '{}'. Device not registered",
150                     new String(packet.getData(), 0, packet.getLength() - 1, StandardCharsets.UTF_8));
151         } else {
152             String thingName = createThingName(packet.getData());
153             ThingUID thingUID = new ThingUID(THING_TYPE_BULB, thingName.toLowerCase());
154             thingDiscovered(DiscoveryResultBuilder.create(thingUID).withLabel(thingName)
155                     .withProperties(collectProperties(ipAddress, stringToMac(packet.getData(), packet.getLength())))
156                     .build());
157         }
158     }
159
160     /**
161      * Creates a name for the Feican device. The name is derived from the mac address (last 4 bytes) and prefixed with
162      * FC_. This matches the wifi host it starts when not yet configured.
163      *
164      * @param byteMac mac address in bytes
165      * @return the name for the device
166      */
167     private String createThingName(final byte[] byteMac) {
168         return FEICAN_NAME_PREFIX + new String(byteMac, 8, 4, StandardCharsets.UTF_8);
169     }
170
171     /**
172      * Converts a byte representation of a mac address to a real mac address.
173      *
174      * @param stringMac byte representation of a mac address
175      * @return real mac address
176      */
177     private String stringToMac(byte[] data, int length) {
178         return new String(data, 0, length - 1, StandardCharsets.UTF_8).replaceAll("(..)(?!$)", "$1:");
179     }
180
181     /**
182      * Collects properties into a map.
183      *
184      * @param ipAddress IP address of the thing
185      * @param mac mac address of the thing
186      * @return map with properties
187      */
188     private Map<String, Object> collectProperties(String ipAddress, String mac) {
189         final Map<String, Object> properties = new TreeMap<>();
190         properties.put(CONFIG_IP, ipAddress);
191         properties.put(PROPERTY_MAC, mac);
192         return properties;
193     }
194 }