added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.smartthings-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-smartthings" description="Samsung Smartthings Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.smartthings/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SmartthingsBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsBindingConstants {
public static final String BINDING_ID = "smartthings";
// List of Bridge Type UIDs
public static final ThingTypeUID THING_TYPE_SMARTTHINGS = new ThingTypeUID(BINDING_ID, "smartthings");
// List of all Thing Type UIDs
// I tried to replace this with a dynamic processing of the thing-types.xml file using the ThingTypeRegistry
// But the HandlerFactory wants to start checking on things before that code runs. So, back to a hard coded list
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream.of(
new ThingTypeUID(BINDING_ID, "accelerationSensor"), new ThingTypeUID(BINDING_ID, "airConditionerMode"),
new ThingTypeUID(BINDING_ID, "alarm"), new ThingTypeUID(BINDING_ID, "battery"),
new ThingTypeUID(BINDING_ID, "beacon"), new ThingTypeUID(BINDING_ID, "bulb"),
new ThingTypeUID(BINDING_ID, "button"), new ThingTypeUID(BINDING_ID, "carbonDioxideMeasurement"),
new ThingTypeUID(BINDING_ID, "carbonMonoxideDetector"), new ThingTypeUID(BINDING_ID, "color"),
new ThingTypeUID(BINDING_ID, "colorControl"), new ThingTypeUID(BINDING_ID, "colorTemperature"),
new ThingTypeUID(BINDING_ID, "consumable"), new ThingTypeUID(BINDING_ID, "contactSensor"),
new ThingTypeUID(BINDING_ID, "doorControl"), new ThingTypeUID(BINDING_ID, "energyMeter"),
new ThingTypeUID(BINDING_ID, "dryerMode"), new ThingTypeUID(BINDING_ID, "dryerOperatingState"),
new ThingTypeUID(BINDING_ID, "estimatedTimeOfArrival"), new ThingTypeUID(BINDING_ID, "garageDoorControl"),
new ThingTypeUID(BINDING_ID, "holdableButton"), new ThingTypeUID(BINDING_ID, "illuminanceMeasurement"),
new ThingTypeUID(BINDING_ID, "imageCapture"), new ThingTypeUID(BINDING_ID, "indicator"),
new ThingTypeUID(BINDING_ID, "infraredLevel"), new ThingTypeUID(BINDING_ID, "light"),
new ThingTypeUID(BINDING_ID, "lock"), new ThingTypeUID(BINDING_ID, "lockOnly"),
new ThingTypeUID(BINDING_ID, "mediaController"), new ThingTypeUID(BINDING_ID, "motionSensor"),
new ThingTypeUID(BINDING_ID, "musicPlayer"), new ThingTypeUID(BINDING_ID, "outlet"),
new ThingTypeUID(BINDING_ID, "pHMeasurement"), new ThingTypeUID(BINDING_ID, "powerMeter"),
new ThingTypeUID(BINDING_ID, "powerSource"), new ThingTypeUID(BINDING_ID, "presenceSensor"),
new ThingTypeUID(BINDING_ID, "relativeHumidityMeasurement"), new ThingTypeUID(BINDING_ID, "relaySwitch"),
new ThingTypeUID(BINDING_ID, "shockSensor"), new ThingTypeUID(BINDING_ID, "signalStrength"),
new ThingTypeUID(BINDING_ID, "sleepSensor"), new ThingTypeUID(BINDING_ID, "smokeDetector"),
new ThingTypeUID(BINDING_ID, "soundPressureLevel"), new ThingTypeUID(BINDING_ID, "soundSensor"),
new ThingTypeUID(BINDING_ID, "speechRecognition"), new ThingTypeUID(BINDING_ID, "stepSensor"),
new ThingTypeUID(BINDING_ID, "switch"), new ThingTypeUID(BINDING_ID, "switchLevel"),
new ThingTypeUID(BINDING_ID, "tamperAlert"), new ThingTypeUID(BINDING_ID, "temperatureMeasurement"),
new ThingTypeUID(BINDING_ID, "thermostat"), new ThingTypeUID(BINDING_ID, "thermostatCoolingSetpoint"),
new ThingTypeUID(BINDING_ID, "thermostatFanMode"),
new ThingTypeUID(BINDING_ID, "thermostatHeatingSetpoint"), new ThingTypeUID(BINDING_ID, "thermostatMode"),
new ThingTypeUID(BINDING_ID, "thermostatOperatingState"),
new ThingTypeUID(BINDING_ID, "thermostatSetpoint"), new ThingTypeUID(BINDING_ID, "threeAxis"),
new ThingTypeUID(BINDING_ID, "timedSession"), new ThingTypeUID(BINDING_ID, "touchSensor"),
new ThingTypeUID(BINDING_ID, "ultravioletIndex"), new ThingTypeUID(BINDING_ID, "valve"),
new ThingTypeUID(BINDING_ID, "voltageMeasurement"), new ThingTypeUID(BINDING_ID, "washerMode"),
new ThingTypeUID(BINDING_ID, "washerOperatingState"), new ThingTypeUID(BINDING_ID, "waterSensor"),
new ThingTypeUID(BINDING_ID, "windowShade")).collect(Collectors.toSet()));
// Event Handler Topics
public static final String STATE_EVENT_TOPIC = "org/openhab/binding/smartthings/state";
public static final String DISCOVERY_EVENT_TOPIC = "org/openhab/binding/smartthings/discovery";
// Bridge config properties
public static final String IP_ADDRESS = "ipAddress";
public static final String PORT = "port";
// Thing config properties
public static final String SMARTTHINGS_NAME = "smartthingsName";
public static final String THING_TIMEOUT = "timeout";
}

View File

@@ -0,0 +1,196 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal;
import static org.openhab.binding.smartthings.internal.SmartthingsBindingConstants.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.binding.smartthings.internal.handler.SmartthingsBridgeHandler;
import org.openhab.binding.smartthings.internal.handler.SmartthingsThingHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link SmartthingsHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
@Component(service = { ThingHandlerFactory.class,
EventHandler.class }, immediate = true, configurationPid = "binding.smarthings", property = "event.topics=org/openhab/binding/smartthings/state")
public class SmartthingsHandlerFactory extends BaseThingHandlerFactory implements ThingHandlerFactory, EventHandler {
private final Logger logger = LoggerFactory.getLogger(SmartthingsHandlerFactory.class);
private @Nullable SmartthingsBridgeHandler bridgeHandler = null;
private @Nullable ThingUID bridgeUID;
private Gson gson;
private List<SmartthingsThingHandler> thingHandlers = Collections
.synchronizedList(new ArrayList<SmartthingsThingHandler>());
private @NonNullByDefault({}) HttpClient httpClient;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return THING_TYPE_SMARTTHINGS.equals(thingTypeUID) || SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
public SmartthingsHandlerFactory() {
// Get a Gson instance
gson = new Gson();
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_SMARTTHINGS)) {
// This binding only supports one bridge. If the user tries to add a second bridge register and error and
// ignore
if (bridgeHandler != null) {
logger.warn(
"The Smartthings binding only supports one bridge. Please change your configuration to only use one Bridge. This bridge {} will be ignored.",
thing.getUID().getAsString());
return null;
}
bridgeHandler = new SmartthingsBridgeHandler((Bridge) thing, this, bundleContext);
bridgeUID = thing.getUID();
logger.debug("SmartthingsHandlerFactory created BridgeHandler for {}", thingTypeUID.getAsString());
return bridgeHandler;
} else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
// Everything but the bridge is handled by this one handler
// Make sure this thing belongs to the registered Bridge
if (bridgeUID != null && !bridgeUID.equals(thing.getBridgeUID())) {
logger.warn("Thing: {} is being ignored because it does not belong to the registered bridge.",
thing.getLabel());
return null;
}
SmartthingsThingHandler thingHandler = new SmartthingsThingHandler(thing, this);
thingHandlers.add(thingHandler);
logger.debug("SmartthingsHandlerFactory created ThingHandler for {}, {}",
thing.getConfiguration().get("smartthingsName"), thing.getUID().getAsString());
return thingHandler;
}
return null;
}
/**
* Send a command to the Smartthings Hub
*
* @param path http path which tells Smartthings what to execute
* @param data data to send
* @return Response from Smartthings
* @throws InterruptedException
* @throws TimeoutException
* @throws ExecutionException
*/
public void sendDeviceCommand(String path, int timeout, String data)
throws InterruptedException, TimeoutException, ExecutionException {
ContentResponse response = httpClient
.newRequest(bridgeHandler.getSmartthingsIp(), bridgeHandler.getSmartthingsPort())
.timeout(timeout, TimeUnit.SECONDS).path(path).method(HttpMethod.POST)
.content(new StringContentProvider(data), "application/json").send();
int status = response.getStatus();
if (status == 202) {
logger.debug(
"Sent message \"{}\" with path \"{}\" to the Smartthings hub, received HTTP status {} (This is the normal code from Smartthings)",
data, path, status);
} else {
logger.warn("Sent message \"{}\" with path \"{}\" to the Smartthings hub, received HTTP status {}", data,
path, status);
}
}
/**
* Messages sent to the Smartthings binding from the hub via the SmartthingsServlet arrive here and are then
* dispatched to the correct thing's handleStateMessage function
*
* @param event The event sent
*/
@Override
public synchronized void handleEvent(@Nullable Event event) {
if (event != null) {
String data = (String) event.getProperty("data");
SmartthingsStateData stateData = new SmartthingsStateData();
stateData = gson.fromJson(data, stateData.getClass());
SmartthingsThingHandler handler = findHandler(stateData);
if (handler != null) {
handler.handleStateMessage(stateData);
}
}
}
private @Nullable SmartthingsThingHandler findHandler(SmartthingsStateData stateData) {
synchronized (thingHandlers) {
for (SmartthingsThingHandler handler : thingHandlers) {
if (handler.getSmartthingsName().equals(stateData.deviceDisplayName)) {
for (Channel ch : handler.getThing().getChannels()) {
String chId = ch.getUID().getId();
if (chId.equals(stateData.capabilityAttribute)) {
return handler;
}
}
}
}
}
logger.warn(
"Unable to locate handler for display name: {} with attribute: {}. If this thing is included in your OpenHabAppV2 SmartApp in the Smartthings App on your phone it must also be configured in openHAB",
stateData.deviceDisplayName, stateData.capabilityAttribute);
return null;
}
@Reference
protected void setHttpClientFactory(HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
protected void unsetHttpClientFactory() {
this.httpClient = null;
}
@Nullable
public SmartthingsBridgeHandler getBridgeHandler() {
return bridgeHandler;
}
}

View File

@@ -0,0 +1,174 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal;
import static org.openhab.binding.smartthings.internal.SmartthingsBindingConstants.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* Receives all Http data from the Smartthings Hub
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
@SuppressWarnings("serial")
@Component(immediate = true, service = HttpServlet.class)
public class SmartthingsServlet extends HttpServlet {
private static final String PATH = "/smartthings";
private final Logger logger = LoggerFactory.getLogger(SmartthingsServlet.class);
private @NonNullByDefault({}) HttpService httpService;
private @Nullable EventAdmin eventAdmin;
private Gson gson = new Gson();
@Activate
protected void activate(Map<String, Object> config) {
if (httpService == null) {
logger.warn("SmartthingsServlet.activate: httpService is unexpectedly null");
return;
}
try {
Dictionary<String, String> servletParams = new Hashtable<String, String>();
httpService.registerServlet(PATH, this, servletParams, httpService.createDefaultHttpContext());
} catch (ServletException | NamespaceException e) {
logger.warn("Could not start Smartthings servlet service: {}", e.getMessage());
}
}
@Deactivate
protected void deactivate(ComponentContext componentContext) {
if (httpService != null) {
try {
httpService.unregister(PATH);
} catch (IllegalArgumentException ignored) {
}
}
}
@Reference
protected void setHttpService(HttpService httpService) {
this.httpService = httpService;
}
protected void unsetHttpService(HttpService httpService) {
this.httpService = null;
}
@Reference
protected void setEventAdmin(EventAdmin eventAdmin) {
this.eventAdmin = eventAdmin;
}
protected void unsetEventAdmin(EventAdmin eventAdmin) {
this.eventAdmin = null;
}
@Override
protected void service(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null) {
logger.debug("SmartthingsServlet.service unexpectedly received a null request. Request not processed");
return;
}
String path = req.getRequestURI();
// See what is in the path
String[] pathParts = path.replace(PATH + "/", "").split("/");
if (pathParts.length != 1) {
logger.warn(
"Smartthing servlet received a path with zero or more than one parts. Only one part is allowed. path {}",
path);
return;
}
BufferedReader rdr = new BufferedReader(req.getReader());
String s = rdr.lines().collect(Collectors.joining());
switch (pathParts[0]) {
case "state":
// This is device state info returned from Smartthings
logger.debug("Smartthing servlet processing \"state\" request. data: {}", s);
publishEvent(STATE_EVENT_TOPIC, "data", s);
break;
case "discovery":
// This is discovery data returned from Smartthings
logger.trace("Smartthing servlet processing \"discovery\" request. data: {}", s);
publishEvent(DISCOVERY_EVENT_TOPIC, "data", s);
break;
case "error":
// This is an error message from smartthings
Map<String, String> map = new HashMap<String, String>();
map = gson.fromJson(s, map.getClass());
logger.warn("Error message from Smartthings: {}", map.get("message"));
break;
default:
logger.warn("Smartthings servlet received a path that is not supported {}", pathParts[0]);
}
// A user @fx submitted a pull request stating:
// It appears that the HubAction queue will choke for a timeout of 6-8s~ if a http action doesn't return a body
// (or possibly on the 204 http code, I didn't test them separately.)
// I tested the following scenarios:
// 1. Return status 204 with a response of OK
// 2. Return status 202 with no response
// 3. No response.
// In all cases the time was about the same - 3.5 sec/request
// Both the 202 and 204 responses resulted in the hub logging an error: received a request with an unknown path:
// HTTP/1.1 200 OK, content-Length: 0
// Therefore I am opting to return nothing since no error message occurs.
// resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
// resp.setStatus(HttpServletResponse.SC_OK);
// resp.getWriter().write("OK");
// resp.getWriter().flush();
// resp.getWriter().close();
logger.trace("Smartthings servlet returning.");
return;
}
private void publishEvent(String topic, String name, String data) {
Dictionary<String, String> props = new Hashtable<String, String>();
props.put(name, data);
Event event = new Event(topic, props);
if (eventAdmin != null) {
eventAdmin.postEvent(event);
} else {
logger.debug("SmartthingsServlet:publishEvent eventAdmin is unexpectedly null");
}
}
}

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.converter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converter class for Smartthings capability "Color Control".
* In this case the color being delivered by Smartthings is in the for #hhssbb where hh=hue in hex, ss=saturation in hex
* and bb=brightness in hex
* And, the hue is a value from 0 to 100% but openHAB expects the hue in 0 to 360
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsColor100Converter extends SmartthingsConverter {
private Pattern rgbInputPattern = Pattern.compile("^#[0-9a-fA-F]{6}");
private final Logger logger = LoggerFactory.getLogger(SmartthingsColor100Converter.class);
public SmartthingsColor100Converter(Thing thing) {
super(thing);
}
@Override
public String convertToSmartthings(ChannelUID channelUid, Command command) {
String jsonMsg;
// The command should be of HSBType. The hue component needs to be divided by 3.6 to convert 0-360 degrees to
// 0-100 percent
// The easiest way to do this is to create a new HSBType with the hue component changed.
if (command instanceof HSBType) {
HSBType hsb = (HSBType) command;
double hue = Math.round((hsb.getHue().doubleValue() / 3.60)); // add .5 to round
long hueInt = (long) hue;
HSBType hsb100 = new HSBType(new DecimalType(hueInt), hsb.getSaturation(), hsb.getBrightness());
// now use the default converter to convert to a JSON string
jsonMsg = defaultConvertToSmartthings(channelUid, hsb100);
} else {
jsonMsg = defaultConvertToSmartthings(channelUid, command);
}
return jsonMsg;
}
/*
* (non-Javadoc)
*
* @see org.openhab.binding.smartthings.internal.converter.SmartthingsConverter#convertToOpenHab(java.lang.String,
* org.openhab.binding.smartthings.internal.SmartthingsStateData)
*/
@Override
public State convertToOpenHab(@Nullable String acceptedChannelType, SmartthingsStateData dataFromSmartthings) {
// The color value from Smartthings will look like "#123456" which is the RGB color
// This needs to be converted into HSB type
String value = dataFromSmartthings.value;
if (value == null) {
logger.warn("Failed to convert color {} because Smartthings returned a null value.",
dataFromSmartthings.deviceDisplayName);
return UnDefType.UNDEF;
}
// If the bulb is off the value maybe null, so better check
State state;
// First verify the format the string is valid
Matcher matcher = rgbInputPattern.matcher(value);
if (!matcher.matches()) {
logger.warn(
"The \"value\" in the following message is not a valid color. Expected a value like \"#123456\" instead of {}",
dataFromSmartthings.toString());
return UnDefType.UNDEF;
}
// Get the RGB colors
int rgb[] = new int[3];
for (int i = 0, pos = 1; i < 3; i++, pos += 2) {
String c = value.substring(pos, pos + 2);
rgb[i] = Integer.parseInt(c, 16);
}
// Convert to state
state = HSBType.fromRGB(rgb[0], rgb[1], rgb[2]);
return state;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.converter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converter class for Smartthings "Color" capability and not the "Color Control" capability.
* The Smartthings Color capability seems to be a later capability where the hue is in the standard 0 - 360 range and
* therefore doesn't need to be converted for openHAB
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsColorConverter extends SmartthingsConverter {
private Pattern rgbInputPattern = Pattern.compile("^#[0-9a-fA-F]{6}");
private final Logger logger = LoggerFactory.getLogger(SmartthingsColorConverter.class);
public SmartthingsColorConverter(Thing thing) {
super(thing);
}
@Override
public String convertToSmartthings(ChannelUID channelUid, Command command) {
String jsonMsg = defaultConvertToSmartthings(channelUid, command);
return jsonMsg;
}
/*
* (non-Javadoc)
*
* @see org.openhab.binding.smartthings.internal.converter.SmartthingsConverter#convertToOpenHab(java.lang.String,
* org.openhab.binding.smartthings.internal.SmartthingsStateData)
*/
@Override
public State convertToOpenHab(@Nullable String acceptedChannelType, SmartthingsStateData dataFromSmartthings) {
// The color value from Smartthings will look like "#123456" which is the RGB color
// This needs to be converted into HSB type
String value = dataFromSmartthings.value;
if (value == null) {
logger.warn("Failed to convert color {} because Smartthings returned a null value.",
dataFromSmartthings.deviceDisplayName);
return UnDefType.UNDEF;
}
// First verify the format the string is valid
Matcher matcher = rgbInputPattern.matcher(value);
if (!matcher.matches()) {
logger.warn(
"The \"value\" in the following message is not a valid color. Expected a value like \"#123456\" instead of {}",
dataFromSmartthings.toString());
return UnDefType.UNDEF;
}
// Get the RGB colors
int rgb[] = new int[3];
for (int i = 0, pos = 1; i < 3; i++, pos += 2) {
String c = value.substring(pos, pos + 2);
rgb[i] = Integer.parseInt(c, 16);
}
// Convert to state
State state = HSBType.fromRGB(rgb[0], rgb[1], rgb[2]);
return state;
}
}

View File

@@ -0,0 +1,217 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.converter;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.binding.smartthings.internal.handler.SmartthingsThingConfig;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringListType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base converter class.
* The converter classes are responsible for converting "state" messages from the smartthings hub into openHAB States.
* And, converting handler.handleCommand() into messages to be sent to smartthings
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public abstract class SmartthingsConverter {
private final Logger logger = LoggerFactory.getLogger(SmartthingsConverter.class);
protected String smartthingsName;
protected String thingTypeId;
SmartthingsConverter(Thing thing) {
smartthingsName = thing.getConfiguration().as(SmartthingsThingConfig.class).smartthingsName;
thingTypeId = thing.getThingTypeUID().getId();
}
public abstract String convertToSmartthings(ChannelUID channelUid, Command command);
public abstract State convertToOpenHab(@Nullable String acceptedChannelType,
SmartthingsStateData dataFromSmartthings);
/**
* Provide a default converter in the base call so it can be used in sub-classes if needed
*
* @param command
* @return The json string to send to Smartthings
*/
protected String defaultConvertToSmartthings(ChannelUID channelUid, Command command) {
String value;
if (command instanceof DateTimeType) {
DateTimeType dt = (DateTimeType) command;
value = dt.format("%m/%d/%Y %H.%M.%S");
} else if (command instanceof HSBType) {
HSBType hsb = (HSBType) command;
value = String.format("[%d, %d, %d ]", hsb.getHue().intValue(), hsb.getSaturation().intValue(),
hsb.getBrightness().intValue());
} else if (command instanceof DecimalType) {
value = command.toString();
} else if (command instanceof IncreaseDecreaseType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof NextPreviousType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof OnOffType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof OpenClosedType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof PercentType) {
value = command.toString();
} else if (command instanceof PointType) { // There is not a comparable type in Smartthings, log and send value
logger.warn(
"Warning - PointType Command is not supported by Smartthings. Please configure to use a different command type. CapabilityKey: {}, displayName: {}, capabilityAttribute {}",
thingTypeId, smartthingsName, channelUid.getId());
value = command.toFullString();
} else if (command instanceof RefreshType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof RewindFastforwardType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof StopMoveType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof PlayPauseType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else if (command instanceof StringListType) {
value = surroundWithQuotes(command.toString());
} else if (command instanceof StringType) {
value = surroundWithQuotes(command.toString());
} else if (command instanceof UpDownType) { // Need to surround with double quotes
value = surroundWithQuotes(command.toString().toLowerCase());
} else {
logger.warn(
"Warning - The Smartthings converter does not know how to handle the {} command. The Smartthingsonverter class should be updated. CapabilityKey: {}, displayName: {}, capabilityAttribute {}",
command.getClass().getName(), thingTypeId, smartthingsName, channelUid.getId());
value = command.toString().toLowerCase();
}
String jsonMsg = String.format(
"{\"capabilityKey\": \"%s\", \"deviceDisplayName\": \"%s\", \"capabilityAttribute\": \"%s\", \"value\": %s}",
thingTypeId, smartthingsName, channelUid.getId(), value);
return jsonMsg;
}
protected String surroundWithQuotes(String param) {
return (new StringBuilder()).append('"').append(param).append('"').toString();
}
protected State defaultConvertToOpenHab(@Nullable String acceptedChannelType,
SmartthingsStateData dataFromSmartthings) {
// If there is no stateMap the just return null State
if (acceptedChannelType == null) {
return UnDefType.NULL;
}
String deviceType = dataFromSmartthings.capabilityAttribute;
Object deviceValue = dataFromSmartthings.value;
// deviceValue can be null, handle that up front
if (deviceValue == null) {
return UnDefType.NULL;
}
switch (acceptedChannelType) {
case "Color":
logger.warn(
"Conversion of Color is not supported by the default Smartthings to opemHAB converter. The ThingType should specify an appropriate converter. Device name: {}, Attribute: {}.",
dataFromSmartthings.deviceDisplayName, deviceType);
return UnDefType.UNDEF;
case "Contact":
return "open".equals(deviceValue) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
case "DateTime":
return UnDefType.UNDEF;
case "Dimmer":
// The value coming in should be a number
if (deviceValue instanceof String) {
return new PercentType((String) deviceValue);
} else {
logger.warn("Failed to convert {} with a value of {} from class {} to an appropriate type.",
deviceType, deviceValue, deviceValue.getClass().getName());
return UnDefType.UNDEF;
}
case "Number":
if (deviceValue instanceof String) {
return new DecimalType(Double.parseDouble((String) deviceValue));
} else if (deviceValue instanceof Double) {
return new DecimalType((Double) deviceValue);
} else if (deviceValue instanceof Long) {
return new DecimalType((Long) deviceValue);
} else {
logger.warn("Failed to convert Number {} with a value of {} from class {} to an appropriate type.",
deviceType, deviceValue, deviceValue.getClass().getName());
return UnDefType.UNDEF;
}
case "Player":
logger.warn("Conversion of Player is not currently supported. Need to provide support for message {}.",
deviceValue);
return UnDefType.UNDEF;
case "Rollershutter":
return "open".equals(deviceValue) ? UpDownType.DOWN : UpDownType.UP;
case "String":
return new StringType((String) deviceValue);
case "Switch":
return "on".equals(deviceValue) ? OnOffType.ON : OnOffType.OFF;
// Vector3 can't be triggered now but keep it to handle acceleration device
case "Vector3":
// This is a weird result from Smartthings. If the messages is from a "state" request the result will
// look like: "value":{"z":22,"y":-36,"x":-987}
// But if the result is from sensor change via a subscription to a a threeAxis device the results will
// be a String of the format "value":"-873,-70,484"
// which GSON returns as a LinkedTreeMap
if (deviceValue instanceof String) {
return new StringType((String) deviceValue);
} else if (deviceValue instanceof Map<?, ?>) {
Map<String, String> map = (Map<String, String>) deviceValue;
String s = String.format("%.0f,%.0f,%.0f", map.get("x"), map.get("y"), map.get("z"));
return new StringType(s);
} else {
logger.warn(
"Unable to convert {} which should be in Smartthings Vector3 format to a string. The returned datatype from Smartthings is {}.",
deviceType, deviceValue.getClass().getName());
return UnDefType.UNDEF;
}
default:
logger.warn("No type defined to convert {} with a value of {} from class {} to an appropriate type.",
deviceType, deviceValue, deviceValue.getClass().getName());
return UnDefType.UNDEF;
}
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.converter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* This "Converter" is assigned to a channel when a special converter is not needed.
* A channel specific converter is specified in the thing-type channel property smartthings-converter then that channel
* is used.
* If a channel specific converter is not found a convert based on the channel ID is used.
* If there is no convert found then this Default converter is used.
* Yes, it would be possible to change the SamrtthingsConverter class to not being abstract and implement these methods
* there. But, this makes it explicit that the default converter is being used.
* See SmartthingsThingHandler.initialize() for details
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsDefaultConverter extends SmartthingsConverter {
public SmartthingsDefaultConverter(Thing thing) {
super(thing);
}
@Override
public String convertToSmartthings(ChannelUID channelUid, Command command) {
String jsonMsg = defaultConvertToSmartthings(channelUid, command);
return jsonMsg;
}
@Override
public State convertToOpenHab(@Nullable String acceptedChannelType, SmartthingsStateData dataFromSmartthings) {
State state = defaultConvertToOpenHab(acceptedChannelType, dataFromSmartthings);
return state;
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.converter;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converter class for Smartthings capability "Color Control".
* The Smartthings "Color Control" capability represents the hue values in the 0-100% range. OH2 uses 0-360 degrees
* For this converter only the hue is coming into openHAB and it is a number
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsHue100Converter extends SmartthingsConverter {
private final Logger logger = LoggerFactory.getLogger(SmartthingsHue100Converter.class);
public SmartthingsHue100Converter(Thing thing) {
super(thing);
}
@Override
public String convertToSmartthings(ChannelUID channelUid, Command command) {
String jsonMsg;
if (command instanceof HSBType) {
HSBType hsb = (HSBType) command;
double hue = hsb.getHue().doubleValue() / 3.60;
String value = String.format("[%.0f, %d, %d ]", hue, hsb.getSaturation().intValue(),
hsb.getBrightness().intValue());
jsonMsg = String.format(
"{\"capabilityKey\": \"%s\", \"deviceDisplayName\": \"%s\", \"capabilityAttribute\": \"%s\", \"value\": %s}",
thingTypeId, smartthingsName, channelUid.getId(), value);
} else {
jsonMsg = defaultConvertToSmartthings(channelUid, command);
}
return jsonMsg;
}
@Override
public State convertToOpenHab(@Nullable String acceptedChannelType, SmartthingsStateData dataFromSmartthings) {
// Here we have to multiply the value from Smartthings by 3.6 to convert from 0-100 to 0-360
String deviceType = dataFromSmartthings.capabilityAttribute;
Object deviceValue = dataFromSmartthings.value;
if (deviceValue == null) {
logger.warn("Failed to convert Number {} because Smartthings returned a null value.", deviceType);
return UnDefType.UNDEF;
}
if ("Number".contentEquals(acceptedChannelType)) {
if (deviceValue instanceof String) {
double d = Double.parseDouble((String) deviceValue);
d *= 3.6;
return new DecimalType(d);
} else if (deviceValue instanceof Long) {
double d = ((Long) deviceValue).longValue();
d *= 3.6;
return new DecimalType(d);
} else if (deviceValue instanceof BigDecimal) {
double d = ((BigDecimal) deviceValue).doubleValue();
d *= 3.6;
return new DecimalType(d);
} else if (deviceValue instanceof Number) {
double d = ((Number) deviceValue).doubleValue();
d *= 3.6;
return new DecimalType(d);
} else {
logger.warn("Failed to convert Number {} with a value of {} from class {} to an appropriate type.",
deviceType, deviceValue, deviceValue.getClass().getName());
return UnDefType.UNDEF;
}
} else {
return defaultConvertToOpenHab(acceptedChannelType, dataFromSmartthings);
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.converter;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* Converter class for Door Control.
* This can't use the default because when closing the door the command that comes in as "closed" but "close" needs to
* be
* sent to Smartthings
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsOpenCloseControlConverter extends SmartthingsConverter {
public SmartthingsOpenCloseControlConverter(Thing thing) {
super(thing);
}
@Override
public String convertToSmartthings(ChannelUID channelUid, Command command) {
String smartthingsValue = (command.toString().toLowerCase().equals("open")) ? "open" : "close";
smartthingsValue = surroundWithQuotes(smartthingsValue);
String jsonMsg = String.format("{\"capabilityKey\": \"%s\", \"deviceDisplayName\": \"%s\", \"value\": %s}",
thingTypeId, smartthingsName, smartthingsValue);
return jsonMsg;
}
@Override
public State convertToOpenHab(@Nullable String acceptedChannelType, SmartthingsStateData dataFromSmartthings) {
State state = defaultConvertToOpenHab(acceptedChannelType, dataFromSmartthings);
return state;
}
}

View File

@@ -0,0 +1,214 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.discovery;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.SmartthingsBindingConstants;
import org.openhab.binding.smartthings.internal.SmartthingsHandlerFactory;
import org.openhab.binding.smartthings.internal.dto.SmartthingsDeviceData;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* Smartthings Discovery service
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
@Component(service = { DiscoveryService.class,
EventHandler.class }, immediate = true, configurationPid = "discovery.smartthings", property = "event.topics=org/openhab/binding/smartthings/discovery")
public class SmartthingsDiscoveryService extends AbstractDiscoveryService implements EventHandler {
private static final int DISCOVERY_TIMEOUT_SEC = 30;
private static final int INITIAL_DELAY_SEC = 10; // Delay 10 sec to give time for bridge and things to be created
private static final int SCAN_INTERVAL_SEC = 600;
private final Pattern findIllegalChars = Pattern.compile("[^A-Za-z0-9_-]");
private final Logger logger = LoggerFactory.getLogger(SmartthingsDiscoveryService.class);
private final Gson gson;
private @Nullable SmartthingsHandlerFactory smartthingsHandlerFactory;
private @Nullable ScheduledFuture<?> scanningJob;
/*
* default constructor
*/
public SmartthingsDiscoveryService() {
super(SmartthingsBindingConstants.SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SEC);
gson = new Gson();
}
@Reference
protected void setThingHandlerFactory(ThingHandlerFactory handlerFactory) {
if (handlerFactory instanceof SmartthingsHandlerFactory) {
smartthingsHandlerFactory = (SmartthingsHandlerFactory) handlerFactory;
}
}
protected void unsetThingHandlerFactory(ThingHandlerFactory handlerFactory) {
// Make sure it is this handleFactory that should be unset
if (handlerFactory == smartthingsHandlerFactory) {
this.smartthingsHandlerFactory = null;
}
}
/**
* Called from the UI when starting a search.
*/
@Override
public void startScan() {
sendSmartthingsDiscoveryRequest();
}
/**
* Stops a running scan.
*/
@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan());
}
/**
* Starts background scanning for attached devices.
*/
@Override
protected void startBackgroundDiscovery() {
if (scanningJob == null) {
this.scanningJob = scheduler.scheduleWithFixedDelay(this::sendSmartthingsDiscoveryRequest,
INITIAL_DELAY_SEC, SCAN_INTERVAL_SEC, TimeUnit.SECONDS);
logger.debug("Discovery background scanning job started");
}
}
/**
* Stops background scanning for attached devices.
*/
@Override
protected void stopBackgroundDiscovery() {
final ScheduledFuture<?> currentScanningJob = scanningJob;
if (currentScanningJob != null) {
currentScanningJob.cancel(false);
scanningJob = null;
}
}
/**
* Start the discovery process by sending a discovery request to the Smartthings Hub
*/
private void sendSmartthingsDiscoveryRequest() {
final SmartthingsHandlerFactory currentSmartthingsHandlerFactory = smartthingsHandlerFactory;
if (currentSmartthingsHandlerFactory != null) {
try {
String discoveryMsg = "{\"discovery\": \"yes\"}";
currentSmartthingsHandlerFactory.sendDeviceCommand("/discovery", 5, discoveryMsg);
// Smartthings will not return a response to this message but will send it's response message
// which will get picked up by the SmartthingBridgeHandler.receivedPushMessage handler
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("Attempt to send command to the Smartthings hub failed with: {}", e.getMessage());
}
}
}
/**
* Handle discovery data returned from the Smartthings hub.
* The data is delivered into the SmartthingServlet. From there it is sent here via the Event service
*/
@Override
public void handleEvent(@Nullable Event event) {
if (event == null) {
logger.info("SmartthingsDiscoveryService.handleEvent: event is uexpectedly null");
return;
}
String topic = event.getTopic();
String data = (String) event.getProperty("data");
if (data == null) {
logger.debug("Event received on topic: {} but the data field is null", topic);
return;
} else {
logger.trace("Event received on topic: {}", topic);
}
// The data returned from the Smartthings hub is a list of strings where each
// element is the data for one device. That device string is another json object
List<String> devices = new ArrayList<String>();
devices = gson.fromJson(data, devices.getClass());
for (String device : devices) {
SmartthingsDeviceData deviceData = gson.fromJson(device, SmartthingsDeviceData.class);
createDevice(deviceData);
}
}
/**
* Create a device with the data from the Smartthings hub
*
* @param deviceData Device data from the hub
*/
private void createDevice(SmartthingsDeviceData deviceData) {
logger.trace("Discovery: Creating device: ThingType {} with name {}", deviceData.capability, deviceData.name);
// Build the UID as a string smartthings:{ThingType}:{BridgeName}:{DeviceName}
String name = deviceData.name; // Note: this is necessary for null analysis to work
if (name == null) {
logger.info(
"Unexpectedly received data for a device with no name. Check the Smartthings hub devices and make sure every device has a name");
return;
}
String deviceNameNoSpaces = name.replaceAll("\\s", "_");
String smartthingsDeviceName = findIllegalChars.matcher(deviceNameNoSpaces).replaceAll("");
final SmartthingsHandlerFactory currentSmartthingsHandlerFactory = smartthingsHandlerFactory;
if (currentSmartthingsHandlerFactory == null) {
logger.info(
"SmartthingsDiscoveryService: smartthingshandlerfactory is unexpectedly null, could not create device {}",
deviceData);
return;
}
ThingUID bridgeUid = currentSmartthingsHandlerFactory.getBridgeHandler().getThing().getUID();
String bridgeId = bridgeUid.getId();
String uidStr = String.format("smartthings:%s:%s:%s", deviceData.capability, bridgeId, smartthingsDeviceName);
Map<String, Object> properties = new HashMap<>();
properties.put("smartthingsName", name);
properties.put("deviceId", deviceData.id);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(new ThingUID(uidStr)).withProperties(properties)
.withRepresentationProperty("deviceId").withBridge(bridgeUid).withLabel(name).build();
thingDiscovered(discoveryResult);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.dto;
/**
* Mapping object for data returned from smartthings hub
*
* @author Bob Raker - Initial contribution
*/
public class SmartthingsDeviceData {
public String capability;
public String attribute;
public String name;
public String id;
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("capability :").append(capability);
sb.append(", attribute :").append(attribute);
sb.append(", name: ").append(name);
sb.append(", id: ").append(id);
return sb.toString();
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.dto;
/**
* Data object for smartthings state data
*
* @author Bob Raker - Initial contribution
*/
public class SmartthingsStateData {
public String deviceDisplayName;
public String capabilityAttribute;
public String value;
public SmartthingsStateData() {
// These values will always be overridden when the object is initialized by GSon
deviceDisplayName = "";
capabilityAttribute = "";
value = "";
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(", deviceDisplayName :").append(deviceDisplayName);
sb.append(", capabilityAttribute :").append(capabilityAttribute);
sb.append(", value :").append(value);
return sb.toString();
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration data for Smartthings hub
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsBridgeConfig {
/**
* IP address of smartthings hub
*/
public String smartthingsIp = "";
/**
* Port number of smartthings hub
*/
public int smartthingsPort = -1;
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("smartthingsIp = ").append(smartthingsIp);
sb.append(", smartthingsPort = ").append(smartthingsPort);
return sb.toString();
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Smartthings Bridge messages
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public interface SmartthingsBridgeConfigStatusMessage {
static final String IP_MISSING = "missing-ip-configuration";
static final String PORT_MISSING = "missing-port-configuration";
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.handler;
import java.util.Collection;
import java.util.LinkedList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.smartthings.internal.SmartthingsBindingConstants;
import org.openhab.binding.smartthings.internal.SmartthingsHandlerFactory;
import org.openhab.core.config.core.status.ConfigStatusMessage;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
import org.openhab.core.types.Command;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SmartthingsBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsBridgeHandler extends ConfigStatusBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(SmartthingsBridgeHandler.class);
private SmartthingsBridgeConfig config;
private SmartthingsHandlerFactory smartthingsHandlerFactory;
private BundleContext bundleContext;
public SmartthingsBridgeHandler(Bridge bridge, SmartthingsHandlerFactory smartthingsHandlerFactory,
BundleContext bundleContext) {
super(bridge);
this.smartthingsHandlerFactory = smartthingsHandlerFactory;
this.bundleContext = bundleContext;
config = getThing().getConfiguration().as(SmartthingsBridgeConfig.class);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Commands are handled by the "Things"
}
@Override
public void initialize() {
// Validate the config
if (!validateConfig(this.config)) {
return;
}
updateStatus(ThingStatus.ONLINE);
}
@Override
public void dispose() {
super.dispose();
}
private boolean validateConfig(SmartthingsBridgeConfig config) {
if (config.smartthingsIp.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Smartthings IP address is not specified");
return false;
}
if (config.smartthingsPort <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Smartthings Port is not specified");
return false;
}
return true;
}
public SmartthingsHandlerFactory getSmartthingsHandlerFactory() {
return smartthingsHandlerFactory;
}
public BundleContext getBundleContext() {
return bundleContext;
}
public String getSmartthingsIp() {
return config.smartthingsIp;
}
public int getSmartthingsPort() {
return config.smartthingsPort;
}
@Override
public Collection<ConfigStatusMessage> getConfigStatus() {
Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<ConfigStatusMessage>();
// The IP must be provided
String ip = config.smartthingsIp;
if (ip.isEmpty()) {
configStatusMessages.add(ConfigStatusMessage.Builder.error(SmartthingsBindingConstants.IP_ADDRESS)
.withMessageKeySuffix(SmartthingsBridgeConfigStatusMessage.IP_MISSING)
.withArguments(SmartthingsBindingConstants.IP_ADDRESS).build());
}
// The PORT must be provided
int port = config.smartthingsPort;
if (port <= 0) {
configStatusMessages.add(ConfigStatusMessage.Builder.error(SmartthingsBindingConstants.PORT)
.withMessageKeySuffix(SmartthingsBridgeConfigStatusMessage.PORT_MISSING)
.withArguments(SmartthingsBindingConstants.PORT).build());
}
return configStatusMessages;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration data for Smartthings device
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsThingConfig {
/**
* The user assigned name used in the Smartthings hub (required)
*/
public String smartthingsName = "";
/**
* The device location (optional)
*/
public String smartthingsLocation = "";
/**
* Timeout (defaults to 3 seconds)
*/
public int smartthingsTimeout = 3;
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Smartthings Bridge messages
*
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public interface SmartthingsThingConfigStatusMessage {
static final String SMARTTHINGS_NAME_MISSING = "missing-smartthings-name";
}

View File

@@ -0,0 +1,273 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.smartthings.internal.handler;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.smartthings.internal.SmartthingsBindingConstants;
import org.openhab.binding.smartthings.internal.SmartthingsHandlerFactory;
import org.openhab.binding.smartthings.internal.converter.SmartthingsConverter;
import org.openhab.binding.smartthings.internal.dto.SmartthingsStateData;
import org.openhab.core.config.core.status.ConfigStatusMessage;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.ConfigStatusThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Bob Raker - Initial contribution
*/
@NonNullByDefault
public class SmartthingsThingHandler extends ConfigStatusThingHandler {
private final Logger logger = LoggerFactory.getLogger(SmartthingsThingHandler.class);
private SmartthingsThingConfig config;
private String smartthingsName;
private int timeout;
private SmartthingsHandlerFactory smartthingsHandlerFactory;
private Map<ChannelUID, SmartthingsConverter> converters = new HashMap<ChannelUID, SmartthingsConverter>();
private final String smartthingsConverterName = "smartthings-converter";
public SmartthingsThingHandler(Thing thing, SmartthingsHandlerFactory smartthingsHandlerFactory) {
super(thing);
this.smartthingsHandlerFactory = smartthingsHandlerFactory;
smartthingsName = ""; // Initialize here so it can be NonNull but it should always get a value in initialize()
config = new SmartthingsThingConfig();
}
/**
* Called when openHAB receives a command for this handler
*
* @param channelUID The channel the command was sent to
* @param command The command sent
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Bridge bridge = getBridge();
// Check if the bridge has not been initialized yet
if (bridge == null) {
logger.debug(
"The bridge has not been initialized yet. Can not process command for channel {} with command {}.",
channelUID.getAsString(), command.toFullString());
return;
}
SmartthingsBridgeHandler smartthingsBridgeHandler = (SmartthingsBridgeHandler) bridge.getHandler();
if (smartthingsBridgeHandler != null
&& smartthingsBridgeHandler.getThing().getStatus().equals(ThingStatus.ONLINE)) {
String thingTypeId = thing.getThingTypeUID().getId();
String smartthingsType = getSmartthingsAttributeFromChannel(channelUID);
SmartthingsConverter converter = converters.get(channelUID);
String path;
String jsonMsg;
if (command instanceof RefreshType) {
path = "/state";
// Go to ST hub and ask for current state
jsonMsg = String.format(
"{\"capabilityKey\": \"%s\", \"deviceDisplayName\": \"%s\", \"capabilityAttribute\": \"%s\", \"openHabStartTime\": %d}",
thingTypeId, smartthingsName, smartthingsType, System.currentTimeMillis());
} else {
// Send update to ST hub
path = "/update";
jsonMsg = converter.convertToSmartthings(channelUID, command);
// The smartthings hub won't (can't) return a response to this call. But, it will send a separate
// message back to the SmartthingBridgeHandler.receivedPushMessage handler
}
try {
smartthingsHandlerFactory.sendDeviceCommand(path, timeout, jsonMsg);
// Smartthings will not return a response to this message but will send it's response message
// which will get picked up by the SmartthingBridgeHandler.receivedPushMessage handler
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("Attempt to send command to the Smartthings hub for {} failed with exception: {}",
smartthingsName, e.getMessage());
}
}
}
/**
* Get the Smartthings capability reference "attribute" from the channel properties.
* In OpenHAB each channel id corresponds to the Smartthings attribute. In the ChannelUID the
* channel id is the last segment
*
* @param channelUID
* @return channel id
*/
private String getSmartthingsAttributeFromChannel(ChannelUID channelUID) {
String id = channelUID.getId();
return id;
}
/**
* State messages sent from the hub arrive here, are processed and the openHab state is updated.
*
* @param stateData
*/
public void handleStateMessage(SmartthingsStateData stateData) {
// First locate the channel
Channel matchingChannel = null;
for (Channel ch : thing.getChannels()) {
if (ch.getUID().getAsString().endsWith(stateData.capabilityAttribute)) {
matchingChannel = ch;
break;
}
}
if (matchingChannel == null) {
return;
}
SmartthingsConverter converter = converters.get(matchingChannel.getUID());
// If value from Smartthings is null then stop here
State state;
if (stateData.value != null) {
state = converter.convertToOpenHab(matchingChannel.getAcceptedItemType(), stateData);
} else {
state = UnDefType.NULL;
}
updateState(matchingChannel.getUID(), state);
logger.trace("Smartthings updated State for channel: {} to {}", matchingChannel.getUID().getAsString(),
state.toString());
}
@Override
public void initialize() {
config = getThing().getConfiguration().as(SmartthingsThingConfig.class);
if (!validateConfig(config)) {
return;
}
smartthingsName = config.smartthingsName;
timeout = config.smartthingsTimeout;
// Create converters for each channel
for (Channel ch : thing.getChannels()) {
@Nullable
String converterName = ch.getProperties().get(smartthingsConverterName);
// Will be null if no explicit converter was specified
if (converterName == null || converterName.isEmpty()) {
// A converter was Not specified so use the channel id
converterName = ch.getUID().getId();
}
// Try to get the converter
SmartthingsConverter cvtr = getConverter(converterName);
if (cvtr == null) {
// If there is no channel specific converter the get the "default" converter
cvtr = getConverter("default");
}
if (cvtr != null) {
// cvtr should never be null because there should always be a "default" converter
converters.put(ch.getUID(), cvtr);
}
}
updateStatus(ThingStatus.ONLINE);
}
private @Nullable SmartthingsConverter getConverter(String converterName) {
// Converter name will be a name such as "switch" which has to be converted into the full class name such as
// org.openhab.binding.smartthings.internal.converter.SmartthingsSwitchConveter
StringBuffer converterClassName = new StringBuffer(
"org.openhab.binding.smartthings.internal.converter.Smartthings");
converterClassName.append(Character.toUpperCase(converterName.charAt(0)));
converterClassName.append(converterName.substring(1));
converterClassName.append("Converter");
try {
Constructor<?> constr = Class.forName(converterClassName.toString()).getDeclaredConstructor(Thing.class);
constr.setAccessible(true);
SmartthingsConverter cvtr = (SmartthingsConverter) constr.newInstance(thing);
return cvtr;
} catch (ClassNotFoundException e) {
// Most of the time there is no channel specific converter, the default converter is all that is needed.
logger.trace("No Custom converter exists for {} ({})", converterName, converterClassName);
} catch (NoSuchMethodException e) {
logger.warn("NoSuchMethodException occurred for {} ({}) {}", converterName, converterClassName,
e.getMessage());
} catch (InvocationTargetException e) {
logger.warn("InvocationTargetException occurred for {} ({}) {}", converterName, converterClassName,
e.getMessage());
} catch (IllegalAccessException e) {
logger.warn("IllegalAccessException occurred for {} ({}) {}", converterName, converterClassName,
e.getMessage());
} catch (InstantiationException e) {
logger.warn("InstantiationException occurred for {} ({}) {}", converterName, converterClassName,
e.getMessage());
}
return null;
}
private boolean validateConfig(SmartthingsThingConfig config) {
String name = config.smartthingsName;
if (name.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Smartthings device name is missing");
return false;
}
return true;
}
@Override
public Collection<ConfigStatusMessage> getConfigStatus() {
Collection<ConfigStatusMessage> configStatusMessages = new LinkedList<ConfigStatusMessage>();
// The name must be provided
String stName = config.smartthingsName;
if (stName.isEmpty()) {
configStatusMessages.add(ConfigStatusMessage.Builder.error(SmartthingsBindingConstants.SMARTTHINGS_NAME)
.withMessageKeySuffix(SmartthingsThingConfigStatusMessage.SMARTTHINGS_NAME_MISSING)
.withArguments(SmartthingsBindingConstants.SMARTTHINGS_NAME).build());
}
return configStatusMessages;
}
public String getSmartthingsName() {
return smartthingsName;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("smartthingsName :").append(smartthingsName);
sb.append(", thing UID: ").append(this.thing.getUID());
sb.append(", thing label: ").append(this.thing.getLabel());
return sb.toString();
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="smartthings" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Samsung Smartthings Binding</name>
<description>This is the binding for the Samsung Smartthings hub.</description>
<author>Bob Raker</author>
</binding:binding>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:smartthings:thing-type-parameters">
<parameter name="smartthingsName" type="text" required="true">
<label>Smartthings Name</label>
<description>User assigned Smartthings device name</description>
</parameter>
<parameter name="smartthingsLocation" type="text">
<label>Smartthings Location</label>
<description>Where the device is located</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,7 @@
# Config status messages
config-status.error.missing-ip-configuration=No IP address for the Smartthings bridge has been provided.
config-status.error.missing-port-configuration=No port for the Smartthings bridge has been provided.
config-status.error.missing-smartthings-name=No Smartthings name has been provided
binding.smartthings.name = Smartthings
binding.smartthings.description = Samsung Smartthings hub