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.smartthings.internal.discovery;
15 import java.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
19 import java.util.Objects;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.smartthings.internal.SmartthingsBindingConstants;
29 import org.openhab.binding.smartthings.internal.SmartthingsHubCommand;
30 import org.openhab.binding.smartthings.internal.dto.SmartthingsDeviceData;
31 import org.openhab.core.config.discovery.AbstractDiscoveryService;
32 import org.openhab.core.config.discovery.DiscoveryResult;
33 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
34 import org.openhab.core.config.discovery.DiscoveryService;
35 import org.openhab.core.thing.ThingUID;
36 import org.osgi.service.component.annotations.Component;
37 import org.osgi.service.component.annotations.Reference;
38 import org.osgi.service.event.Event;
39 import org.osgi.service.event.EventHandler;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.Gson;
46 * Smartthings Discovery service
48 * @author Bob Raker - Initial contribution
51 @Component(service = { DiscoveryService.class,
52 EventHandler.class }, configurationPid = "discovery.smartthings", property = "event.topics=org/openhab/binding/smartthings/discovery")
53 public class SmartthingsDiscoveryService extends AbstractDiscoveryService implements EventHandler {
54 private static final int DISCOVERY_TIMEOUT_SEC = 30;
55 private static final int INITIAL_DELAY_SEC = 10; // Delay 10 sec to give time for bridge and things to be created
56 private static final int SCAN_INTERVAL_SEC = 600;
58 private final Pattern findIllegalChars = Pattern.compile("[^A-Za-z0-9_-]");
60 private final Logger logger = LoggerFactory.getLogger(SmartthingsDiscoveryService.class);
62 private final Gson gson;
64 private @Nullable SmartthingsHubCommand smartthingsHubCommand;
66 private @Nullable ScheduledFuture<?> scanningJob;
71 public SmartthingsDiscoveryService() {
72 super(SmartthingsBindingConstants.SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SEC);
77 protected void setSmartthingsHubCommand(SmartthingsHubCommand hubCommand) {
78 smartthingsHubCommand = hubCommand;
81 protected void unsetSmartthingsHubCommand(SmartthingsHubCommand hubCommand) {
82 // Make sure it is this handleFactory that should be unset
83 if (Objects.equals(hubCommand, smartthingsHubCommand)) {
84 this.smartthingsHubCommand = null;
89 * Called from the UI when starting a search.
92 public void startScan() {
93 sendSmartthingsDiscoveryRequest();
97 * Stops a running scan.
100 protected synchronized void stopScan() {
102 removeOlderResults(getTimestampOfLastScan());
106 * Starts background scanning for attached devices.
109 protected void startBackgroundDiscovery() {
110 if (scanningJob == null) {
111 this.scanningJob = scheduler.scheduleWithFixedDelay(this::sendSmartthingsDiscoveryRequest,
112 INITIAL_DELAY_SEC, SCAN_INTERVAL_SEC, TimeUnit.SECONDS);
113 logger.debug("Discovery background scanning job started");
118 * Stops background scanning for attached devices.
121 protected void stopBackgroundDiscovery() {
122 final ScheduledFuture<?> currentScanningJob = scanningJob;
123 if (currentScanningJob != null) {
124 currentScanningJob.cancel(false);
130 * Start the discovery process by sending a discovery request to the Smartthings Hub
132 private void sendSmartthingsDiscoveryRequest() {
133 if (smartthingsHubCommand != null) {
135 String discoveryMsg = "{\"discovery\": \"yes\"}";
136 smartthingsHubCommand.sendDeviceCommand("/discovery", 5, discoveryMsg);
137 // Smartthings will not return a response to this message but will send it's response message
138 // which will get picked up by the SmartthingBridgeHandler.receivedPushMessage handler
139 } catch (InterruptedException | TimeoutException | ExecutionException e) {
140 logger.warn("Attempt to send command to the Smartthings hub failed with: {}", e.getMessage());
146 * Handle discovery data returned from the Smartthings hub.
147 * The data is delivered into the SmartthingServlet. From there it is sent here via the Event service
150 public void handleEvent(@Nullable Event event) {
152 logger.info("SmartthingsDiscoveryService.handleEvent: event is uexpectedly null");
155 String topic = event.getTopic();
156 String data = (String) event.getProperty("data");
158 logger.debug("Event received on topic: {} but the data field is null", topic);
161 logger.trace("Event received on topic: {}", topic);
164 // The data returned from the Smartthings hub is a list of strings where each
165 // element is the data for one device. That device string is another json object
166 List<String> devices = new ArrayList<>();
167 devices = gson.fromJson(data, devices.getClass());
168 for (String device : devices) {
169 SmartthingsDeviceData deviceData = gson.fromJson(device, SmartthingsDeviceData.class);
170 createDevice(Objects.requireNonNull(deviceData));
175 * Create a device with the data from the Smartthings hub
177 * @param deviceData Device data from the hub
179 private void createDevice(SmartthingsDeviceData deviceData) {
180 logger.trace("Discovery: Creating device: ThingType {} with name {}", deviceData.capability, deviceData.name);
182 // Build the UID as a string smartthings:{ThingType}:{BridgeName}:{DeviceName}
183 String name = deviceData.name; // Note: this is necessary for null analysis to work
186 "Unexpectedly received data for a device with no name. Check the Smartthings hub devices and make sure every device has a name");
189 String deviceNameNoSpaces = name.replaceAll("\\s", "_");
190 String smartthingsDeviceName = findIllegalChars.matcher(deviceNameNoSpaces).replaceAll("");
191 if (smartthingsHubCommand == null) {
192 logger.info("SmartthingsHubCommand is unexpectedly null, could not create device {}", deviceData);
195 ThingUID bridgeUid = smartthingsHubCommand.getBridgeUID();
196 String bridgeId = bridgeUid.getId();
197 String uidStr = String.format("smartthings:%s:%s:%s", deviceData.capability, bridgeId, smartthingsDeviceName);
199 Map<String, Object> properties = new HashMap<>();
200 properties.put("smartthingsName", name);
201 properties.put("deviceId", deviceData.id);
203 DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(new ThingUID(uidStr)).withProperties(properties)
204 .withRepresentationProperty("deviceId").withBridge(bridgeUid).withLabel(name).build();
206 thingDiscovered(discoveryResult);