]> git.basschouten.com Git - openhab-addons.git/blob
e287df04e803f7c39d1d85740620f7d3280bd907
[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.tplinksmarthome.internal;
14
15 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeBindingConstants.CONFIG_DEVICE_ID;
16 import static org.openhab.binding.tplinksmarthome.internal.TPLinkSmartHomeThingType.*;
17
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;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30
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;
43
44 /**
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.
47  *
48  * @author Christian Fischer - Initial contribution
49  * @author Hilbrand Bouwkamp - Complete make-over, reorganized code and code cleanup.
50  */
51 @Component(service = { DiscoveryService.class,
52         TPLinkIpAddressService.class }, configurationPid = "discovery.tplinksmarthome")
53 @NonNullByDefault
54 public class TPLinkSmartHomeDiscoveryService extends AbstractDiscoveryService implements TPLinkIpAddressService {
55
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;
60
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<>();
64
65     private final DatagramPacket discoverPacket;
66     private final byte[] buffer = new byte[2048];
67     private @NonNullByDefault({}) DatagramSocket discoverSocket;
68     private @NonNullByDefault({}) ScheduledFuture<?> discoveryJob;
69
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);
76     }
77
78     @Override
79     public @Nullable String getLastKnownIpAddress(String deviceId) {
80         return idInetAddressCache.get(deviceId);
81     }
82
83     @Override
84     protected void startBackgroundDiscovery() {
85         discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH_INTERVAL_MINUTES, TimeUnit.MINUTES);
86     }
87
88     @Override
89     protected void stopBackgroundDiscovery() {
90         stopScan();
91         if (discoveryJob != null && !discoveryJob.isCancelled()) {
92             discoveryJob.cancel(true);
93             discoveryJob = null;
94         }
95     }
96
97     @Override
98     protected void startScan() {
99         logger.debug("Start scan for TP-Link Smart devices.");
100         synchronized (this) {
101             try {
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.
106                 while (true) {
107                     if (discoverSocket == null) {
108                         break;
109                     }
110                     final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
111
112                     discoverSocket.receive(packet);
113                     logger.debug("TP-Link Smart device discovery returned package with length {}", packet.getLength());
114                     if (packet.getLength() > 0) {
115                         detectThing(packet);
116                     }
117                 }
118             } catch (SocketTimeoutException e) {
119                 logger.debug("Discovering poller timeout...");
120             } catch (IOException e) {
121                 logger.debug("Error during discovery: {}", e.getMessage());
122             } finally {
123                 closeDiscoverSocket();
124                 removeOlderResults(getTimestampOfLastScan());
125             }
126         }
127     }
128
129     @Override
130     protected void stopScan() {
131         logger.debug("Stop scan for TP-Link Smart devices.");
132         closeDiscoverSocket();
133         super.stopScan();
134     }
135
136     /**
137      * Opens a {@link DatagramSocket} and sends a packet for discovery of TP-Link Smart Home devices.
138      *
139      * @return Returns the new socket
140      * @throws IOException exception in case sending the packet failed
141      */
142     protected DatagramSocket sendDiscoveryPacket() throws IOException {
143         final DatagramSocket ds = new DatagramSocket(null);
144
145         ds.setBroadcast(true);
146         ds.setSoTimeout(UDP_PACKET_TIMEOUT_MS);
147         ds.send(discoverPacket);
148         logger.trace("Discovery package sent.");
149         return ds;
150     }
151
152     /**
153      * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a
154      * synchronized context.
155      */
156     private void closeDiscoverSocket() {
157         if (discoverSocket != null) {
158             discoverSocket.close();
159             discoverSocket = null;
160         }
161     }
162
163     /**
164      * Detected a device (thing) and get process the data from the device and report it discovered.
165      *
166      * @param packet containing data of detected device
167      * @throws IOException in case decrypting of the data failed
168      */
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();
174
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());
180
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,
186                     sysinfoRaw);
187             final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
188                     .withLabel(sysinfo.getAlias()).withRepresentationProperty(CONFIG_DEVICE_ID)
189                     .withProperties(properties).build();
190             thingDiscovered(discoveryResult);
191         } else {
192             logger.debug("Detected, but ignoring unsupported TP-Link Smart Home device model '{}'", sysinfo.getModel());
193         }
194     }
195
196     /**
197      * Finds the {@link ThingTypeUID} based on the model value returned by the device.
198      *
199      * @param model model value returned by the device
200      * @return {@link ThingTypeUID} or null if device not recognized
201      */
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()))
205                 .findFirst();
206     }
207 }