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.tplinksmarthome.internal;
15 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.CONFIG_DEVICE_ID;
16 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeThingType.*;
18 import java.io.IOException;
19 import java.net.DatagramPacket;
20 import java.net.DatagramSocket;
21 import java.net.InetAddress;
22 import java.net.SocketTimeoutException;
23 import java.net.UnknownHostException;
24 import java.util.Locale;
26 import java.util.Optional;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.tplinksmarthome.internal.model.Sysinfo;
34 import org.openhab.core.config.discovery.AbstractDiscoveryService;
35 import org.openhab.core.config.discovery.DiscoveryResult;
36 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
37 import org.openhab.core.config.discovery.DiscoveryService;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.ThingUID;
40 import org.osgi.service.component.annotations.Component;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * The {@link TPLinkSmartHomeDiscoveryService} detects new Smart Home Bulbs, Plugs and Switches by sending a UDP network
46 * broadcast and parsing the answer into a thing.
48 * @author Christian Fischer - Initial contribution
49 * @author Hilbrand Bouwkamp - Complete make-over, reorganized code and code cleanup.
51 @Component(service = { DiscoveryService.class,
52 TPLinkIpAddressService.class }, configurationPid = "discovery.tplinksmarthome")
54 public class TPLinkSmartHomeDiscoveryService extends AbstractDiscoveryService implements TPLinkIpAddressService {
56 private static final String BROADCAST_IP = "255.255.255.255";
57 private static final int DISCOVERY_TIMEOUT_SECONDS = 8;
58 private static final int UDP_PACKET_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(DISCOVERY_TIMEOUT_SECONDS - 1);
59 private static final long REFRESH_INTERVAL_MINUTES = 1;
61 private final Logger logger = LoggerFactory.getLogger(TPLinkSmartHomeDiscoveryService.class);
62 private final Commands commands = new Commands();
63 private final Map<String, String> idInetAddressCache = new ConcurrentHashMap<>();
65 private final DatagramPacket discoverPacket;
66 private final byte[] buffer = new byte[2048];
67 private @NonNullByDefault({}) DatagramSocket discoverSocket;
68 private @NonNullByDefault({}) ScheduledFuture<?> discoveryJob;
70 public TPLinkSmartHomeDiscoveryService() throws UnknownHostException {
71 super(SUPPORTED_THING_TYPES, DISCOVERY_TIMEOUT_SECONDS);
72 final InetAddress broadcast = InetAddress.getByName(BROADCAST_IP);
73 final byte[] discoverbuffer = CryptUtil.encrypt(Commands.getSysinfo());
74 discoverPacket = new DatagramPacket(discoverbuffer, discoverbuffer.length, broadcast,
75 Connection.TP_LINK_SMART_HOME_PORT);
79 public @Nullable String getLastKnownIpAddress(String deviceId) {
80 return idInetAddressCache.get(deviceId);
84 protected void startBackgroundDiscovery() {
85 discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH_INTERVAL_MINUTES, TimeUnit.MINUTES);
89 protected void stopBackgroundDiscovery() {
91 if (discoveryJob != null && !discoveryJob.isCancelled()) {
92 discoveryJob.cancel(true);
98 protected void startScan() {
99 logger.debug("Start scan for TP-Link Smart devices.");
100 synchronized (this) {
102 idInetAddressCache.clear();
103 discoverSocket = sendDiscoveryPacket();
104 // Runs until the socket call gets a time out and throws an exception. When a time out is triggered it
105 // means no data was present and nothing new to discover.
107 if (discoverSocket == null) {
110 final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
112 discoverSocket.receive(packet);
113 logger.debug("TP-Link Smart device discovery returned package with length {}", packet.getLength());
114 if (packet.getLength() > 0) {
118 } catch (SocketTimeoutException e) {
119 logger.debug("Discovering poller timeout...");
120 } catch (IOException e) {
121 logger.debug("Error during discovery: {}", e.getMessage());
123 closeDiscoverSocket();
124 removeOlderResults(getTimestampOfLastScan());
130 protected void stopScan() {
131 logger.debug("Stop scan for TP-Link Smart devices.");
132 closeDiscoverSocket();
137 * Opens a {@link DatagramSocket} and sends a packet for discovery of TP-Link Smart Home devices.
139 * @return Returns the new socket
140 * @throws IOException exception in case sending the packet failed
142 protected DatagramSocket sendDiscoveryPacket() throws IOException {
143 final DatagramSocket ds = new DatagramSocket(null);
145 ds.setBroadcast(true);
146 ds.setSoTimeout(UDP_PACKET_TIMEOUT_MS);
147 ds.send(discoverPacket);
148 logger.trace("Discovery package sent.");
153 * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a
154 * synchronized context.
156 private void closeDiscoverSocket() {
157 if (discoverSocket != null) {
158 discoverSocket.close();
159 discoverSocket = null;
164 * Detected a device (thing) and get process the data from the device and report it discovered.
166 * @param packet containing data of detected device
167 * @throws IOException in case decrypting of the data failed
169 private void detectThing(DatagramPacket packet) throws IOException {
170 final String ipAddress = packet.getAddress().getHostAddress();
171 final String rawData = CryptUtil.decrypt(packet.getData(), packet.getLength());
172 final Sysinfo sysinfoRaw = commands.getSysinfoReponse(rawData);
173 final Sysinfo sysinfo = sysinfoRaw.getActualSysinfo();
175 logger.trace("Detected TP-Link Smart Home device: {}", rawData);
176 final String deviceId = sysinfo.getDeviceId();
177 logger.debug("TP-Link Smart Home device '{}' with id {} found on {} ", sysinfo.getAlias(), deviceId, ipAddress);
178 idInetAddressCache.put(deviceId, ipAddress);
179 final Optional<TPLinkSmartHomeThingType> thingType = getThingTypeUID(sysinfo.getModel());
181 if (thingType.isPresent()) {
182 final ThingTypeUID thingTypeUID = thingType.get().thingTypeUID();
183 final ThingUID thingUID = new ThingUID(thingTypeUID,
184 deviceId.substring(deviceId.length() - 6, deviceId.length()));
185 final Map<String, Object> properties = PropertiesCollector.collectProperties(thingType.get(), ipAddress,
187 final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
188 .withLabel(sysinfo.getAlias()).withRepresentationProperty(CONFIG_DEVICE_ID)
189 .withProperties(properties).build();
190 thingDiscovered(discoveryResult);
192 logger.debug("Detected, but ignoring unsupported TP-Link Smart Home device model '{}'", sysinfo.getModel());
197 * Finds the {@link ThingTypeUID} based on the model value returned by the device.
199 * @param model model value returned by the device
200 * @return {@link ThingTypeUID} or null if device not recognized
202 private Optional<TPLinkSmartHomeThingType> getThingTypeUID(String model) {
203 final String modelLC = model.toLowerCase(Locale.ENGLISH);
204 return SUPPORTED_THING_TYPES_LIST.stream().filter(type -> modelLC.startsWith(type.thingTypeUID().getId()))