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.handler;
15 import java.lang.reflect.Constructor;
16 import java.lang.reflect.InvocationTargetException;
17 import java.util.Collection;
18 import java.util.HashMap;
19 import java.util.LinkedList;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.smartthings.internal.SmartthingsBindingConstants;
27 import org.openhab.binding.smartthings.internal.SmartthingsHandlerFactory;
28 import org.openhab.binding.smartthings.internal.converter.SmartthingsConverter;
29 import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
30 import org.openhab.core.config.core.status.ConfigStatusMessage;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.ConfigStatusThingHandler;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.State;
41 import org.openhab.core.types.UnDefType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * @author Bob Raker - Initial contribution
49 public class SmartthingsThingHandler extends ConfigStatusThingHandler {
51 private final Logger logger = LoggerFactory.getLogger(SmartthingsThingHandler.class);
53 private SmartthingsThingConfig config;
54 private String smartthingsName;
56 private SmartthingsHandlerFactory smartthingsHandlerFactory;
57 private Map<ChannelUID, SmartthingsConverter> converters = new HashMap<ChannelUID, SmartthingsConverter>();
59 private final String smartthingsConverterName = "smartthings-converter";
61 public SmartthingsThingHandler(Thing thing, SmartthingsHandlerFactory smartthingsHandlerFactory) {
63 this.smartthingsHandlerFactory = smartthingsHandlerFactory;
64 smartthingsName = ""; // Initialize here so it can be NonNull but it should always get a value in initialize()
65 config = new SmartthingsThingConfig();
69 * Called when openHAB receives a command for this handler
71 * @param channelUID The channel the command was sent to
72 * @param command The command sent
75 public void handleCommand(ChannelUID channelUID, Command command) {
76 Bridge bridge = getBridge();
78 // Check if the bridge has not been initialized yet
81 "The bridge has not been initialized yet. Can not process command for channel {} with command {}.",
82 channelUID.getAsString(), command.toFullString());
86 SmartthingsBridgeHandler smartthingsBridgeHandler = (SmartthingsBridgeHandler) bridge.getHandler();
87 if (smartthingsBridgeHandler != null
88 && smartthingsBridgeHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
89 String thingTypeId = thing.getThingTypeUID().getId();
90 String smartthingsType = getSmartthingsAttributeFromChannel(channelUID);
92 SmartthingsConverter converter = converters.get(channelUID);
96 if (command instanceof RefreshType) {
98 // Go to ST hub and ask for current state
99 jsonMsg = String.format(
100 "{\"capabilityKey\": \"%s\", \"deviceDisplayName\": \"%s\", \"capabilityAttribute\": \"%s\", \"openHabStartTime\": %d}",
101 thingTypeId, smartthingsName, smartthingsType, System.currentTimeMillis());
103 // Send update to ST hub
105 jsonMsg = converter.convertToSmartthings(channelUID, command);
107 // The smartthings hub won't (can't) return a response to this call. But, it will send a separate
108 // message back to the SmartthingBridgeHandler.receivedPushMessage handler
112 smartthingsHandlerFactory.sendDeviceCommand(path, timeout, jsonMsg);
113 // Smartthings will not return a response to this message but will send it's response message
114 // which will get picked up by the SmartthingBridgeHandler.receivedPushMessage handler
115 } catch (InterruptedException | TimeoutException | ExecutionException e) {
116 logger.warn("Attempt to send command to the Smartthings hub for {} failed with exception: {}",
117 smartthingsName, e.getMessage());
123 * Get the Smartthings capability reference "attribute" from the channel properties.
124 * In OpenHAB each channel id corresponds to the Smartthings attribute. In the ChannelUID the
125 * channel id is the last segment
130 private String getSmartthingsAttributeFromChannel(ChannelUID channelUID) {
131 String id = channelUID.getId();
136 * State messages sent from the hub arrive here, are processed and the openHab state is updated.
140 public void handleStateMessage(SmartthingsStateData stateData) {
141 // First locate the channel
142 Channel matchingChannel = null;
143 for (Channel ch : thing.getChannels()) {
144 if (ch.getUID().getAsString().endsWith(stateData.capabilityAttribute)) {
145 matchingChannel = ch;
149 if (matchingChannel == null) {
153 SmartthingsConverter converter = converters.get(matchingChannel.getUID());
155 // If value from Smartthings is null then stop here
157 if (stateData.value != null) {
158 state = converter.convertToOpenHab(matchingChannel.getAcceptedItemType(), stateData);
160 state = UnDefType.NULL;
163 updateState(matchingChannel.getUID(), state);
164 logger.trace("Smartthings updated State for channel: {} to {}", matchingChannel.getUID().getAsString(),
169 public void initialize() {
170 config = getThing().getConfiguration().as(SmartthingsThingConfig.class);
171 if (!validateConfig(config)) {
174 smartthingsName = config.smartthingsName;
175 timeout = config.smartthingsTimeout;
177 // Create converters for each channel
178 for (Channel ch : thing.getChannels()) {
180 String converterName = ch.getProperties().get(smartthingsConverterName);
181 // Will be null if no explicit converter was specified
182 if (converterName == null || converterName.isEmpty()) {
183 // A converter was Not specified so use the channel id
184 converterName = ch.getUID().getId();
187 // Try to get the converter
188 SmartthingsConverter cvtr = getConverter(converterName);
190 // If there is no channel specific converter the get the "default" converter
191 cvtr = getConverter("default");
195 // cvtr should never be null because there should always be a "default" converter
196 converters.put(ch.getUID(), cvtr);
200 updateStatus(ThingStatus.ONLINE);
203 private @Nullable SmartthingsConverter getConverter(String converterName) {
204 // Converter name will be a name such as "switch" which has to be converted into the full class name such as
205 // org.openhab.binding.smartthings.internal.converter.SmartthingsSwitchConveter
206 StringBuffer converterClassName = new StringBuffer(
207 "org.openhab.binding.smartthings.internal.converter.Smartthings");
208 converterClassName.append(Character.toUpperCase(converterName.charAt(0)));
209 converterClassName.append(converterName.substring(1));
210 converterClassName.append("Converter");
212 Constructor<?> constr = Class.forName(converterClassName.toString()).getDeclaredConstructor(Thing.class);
213 constr.setAccessible(true);
214 SmartthingsConverter cvtr = (SmartthingsConverter) constr.newInstance(thing);
216 } catch (ClassNotFoundException e) {
217 // Most of the time there is no channel specific converter, the default converter is all that is needed.
218 logger.trace("No Custom converter exists for {} ({})", converterName, converterClassName);
219 } catch (NoSuchMethodException e) {
220 logger.warn("NoSuchMethodException occurred for {} ({}) {}", converterName, converterClassName,
222 } catch (InvocationTargetException e) {
223 logger.warn("InvocationTargetException occurred for {} ({}) {}", converterName, converterClassName,
225 } catch (IllegalAccessException e) {
226 logger.warn("IllegalAccessException occurred for {} ({}) {}", converterName, converterClassName,
228 } catch (InstantiationException e) {
229 logger.warn("InstantiationException occurred for {} ({}) {}", converterName, converterClassName,
235 private boolean validateConfig(SmartthingsThingConfig config) {
236 String name = config.smartthingsName;
237 if (name.isEmpty()) {
238 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
239 "Smartthings device name is missing");
247 public Collection<ConfigStatusMessage> getConfigStatus() {
248 Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<ConfigStatusMessage>();
250 // The name must be provided
251 String stName = config.smartthingsName;
252 if (stName.isEmpty()) {
253 configStatusMessages.add(ConfigStatusMessage.Builder.error(SmartthingsBindingConstants.SMARTTHINGS_NAME)
254 .withMessageKeySuffix(SmartthingsThingConfigStatusMessage.SMARTTHINGS_NAME_MISSING)
255 .withArguments(SmartthingsBindingConstants.SMARTTHINGS_NAME).build());
258 return configStatusMessages;
261 public String getSmartthingsName() {
262 return smartthingsName;
266 public String toString() {
267 StringBuffer sb = new StringBuffer();
268 sb.append("smartthingsName :").append(smartthingsName);
269 sb.append(", thing UID: ").append(this.thing.getUID());
270 sb.append(", thing label: ").append(this.thing.getLabel());
271 return sb.toString();