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.feican.internal;
15 import static org.openhab.binding.feican.internal.FeicanBindingConstants.*;
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;
26 import java.util.TreeMap;
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;
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.
42 * @author Hilbrand Bouwkamp - Initial contribution
45 @Component(service = DiscoveryService.class, configurationPid = "discovery.feican")
46 public class FeicanDiscoveryService extends AbstractDiscoveryService {
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_";
53 private final Logger logger = LoggerFactory.getLogger(FeicanDiscoveryService.class);
56 private final byte[] buffer = new byte[32];
58 private DatagramSocket discoverSocket;
60 public FeicanDiscoveryService() {
61 super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS, false);
65 protected void startScan() {
66 logger.debug("Start scan for Feican devices.");
71 protected void stopScan() {
72 logger.debug("Stop scan for Feican devices.");
73 closeDiscoverSocket();
78 * Performs the actual discovery of Feican devices (things).
80 private void discoverThings() {
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.
88 // Set packet length in case a previous call reduced the size.
89 receivePacket.setLength(buffer.length);
90 if (discoverSocket == null) {
93 discoverSocket.receive(receivePacket);
95 logger.debug("Feican device discovery returned package with length {}", receivePacket.getLength());
96 if (receivePacket.getLength() > 0) {
97 thingDiscovered(receivePacket);
100 } catch (SocketTimeoutException e) {
101 logger.debug("Discovering poller timeout...");
102 } catch (IOException e) {
103 logger.debug("Error during discovery: {}", e.getMessage());
105 closeDiscoverSocket();
106 removeOlderResults(getTimestampOfLastScan());
111 * Opens a {@link DatagramSocket} and sends a packet for discovery of Feican devices.
113 * @throws SocketException
114 * @throws IOException
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));
130 * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a
131 * synchronized context.
133 private void closeDiscoverSocket() {
134 if (discoverSocket != null) {
135 discoverSocket.close();
136 discoverSocket = null;
141 * Register a device (thing) with the discovered properties.
143 * @param packet containing data of detected device
145 private void thingDiscovered(DatagramPacket packet) {
146 final String ipAddress = packet.getAddress().getHostAddress();
147 if (packet.getData().length < 12) {
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));
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())))
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.
164 * @param byteMac mac address in bytes
165 * @return the name for the device
167 private String createThingName(final byte[] byteMac) {
168 return FEICAN_NAME_PREFIX + new String(byteMac, 8, 4, StandardCharsets.UTF_8);
172 * Converts a byte representation of a mac address to a real mac address.
174 * @param stringMac byte representation of a mac address
175 * @return real mac address
177 private String stringToMac(byte[] data, int length) {
178 return new String(data, 0, length - 1, StandardCharsets.UTF_8).replaceAll("(..)(?!$)", "$1:");
182 * Collects properties into a map.
184 * @param ipAddress IP address of the thing
185 * @param mac mac address of the thing
186 * @return map with properties
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);