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,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.io.openhabcloud-${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-misc-openhabcloud" description="openHAB Cloud Connector" version="${project.version}">
<feature>openhab-runtime-base</feature>
<configfile finalname="${openhab.conf}/services/openhabcloud.cfg" override="false">mvn:${project.groupId}/openhab-addons-external/${project.version}/cfg/openhabcloud</configfile>
<bundle dependency="true">mvn:org.json/json/20180813</bundle>
<bundle dependency="true">mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jsr305/3.0.2_1</bundle>
<bundle dependency="true">mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.okhttp/3.8.1_1</bundle>
<bundle dependency="true">mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.okio/1.13.0_1</bundle>
<bundle dependency="true">mvn:org.openhab.osgiify/io.socket.socket.io-client/1.0.0</bundle>
<bundle dependency="true">mvn:org.openhab.osgiify/io.socket.engine.io-client/1.0.0</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.io.openhabcloud/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,121 @@
/**
* 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.io.openhabcloud;
import org.openhab.core.model.script.engine.action.ActionDoc;
import org.openhab.io.openhabcloud.internal.CloudService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class provides static methods that can be used in automation rules
* for sending notifications to the native apps.
*
* @author Victor Belov - Initial contribution
* @author Kai Kreuzer - migrated code to ESH APIs
*
*/
public class NotificationAction {
private static final Logger logger = LoggerFactory.getLogger(NotificationAction.class);
public static CloudService cloudService = null;
/**
* Sends a simple push notification to mobile devices of user
*
* @param userId the cloud user id of the recipient
* @param message the body of the notification
*
*/
@ActionDoc(text = "Sends a push notification to mobile devices of user with userId")
public static void sendNotification(String userId, String message) {
sendNotification(userId, message, null, null);
}
/**
* Sends an advanced push notification to mobile devices of user
*
* @param userId the cloud user id of the recipient
* @param message the body of the notification
* @param icon name for the notification
* @param severity category for the notification
*
*/
@ActionDoc(text = "Sends a push notification to mobile devices of user with userId")
public static void sendNotification(String userId, String message, String icon, String severity) {
logger.debug("sending notification '{}' to user {}", message, userId);
if (cloudService != null) {
cloudService.sendNotification(userId, message, icon, severity);
}
}
/**
* Sends a simple notification to log. Log notifications are not pushed to user
* devices but are shown to all account users in notifications log.
*
* @param message the body of the notification
*
*/
@ActionDoc(text = "Sends a log notification which is shown in notifications log to all account users")
public static void sendLogNotification(String message) {
sendLogNotification(message, null, null);
}
/**
* Sends an advanced notification to log. Log notifications are not pushed to user
* devices but are shown to all account users in notifications log.
*
* @param message the body of the notification
* @param icon name for the notification
* @param severity category for the notification
*
*/
@ActionDoc(text = "Sends a log notification which is shown in notifications log to all account users")
public static void sendLogNotification(String message, String icon, String severity) {
logger.debug("sending log notification '{}'", message);
if (cloudService != null) {
cloudService.sendLogNotification(message, icon, severity);
}
}
/**
* Sends a simple broadcast notification. Broadcast notifications are pushed to all
* mobile devices of all users of the account
*
* @param message the body of the notification
*
*/
@ActionDoc(text = "Sends a broadcast notification to all mobile devices of all account users")
public static void sendBroadcastNotification(String message) {
sendBroadcastNotification(message, null, null);
}
/**
* Sends an advanced broadcast notification. Broadcast notifications are pushed to all
* mobile devices of all users of the account
*
* @param message the body of the notification
* @param icon name for the notification
* @param severity category for the notification
*
*/
@ActionDoc(text = "Sends a push notification to mobile devices of user with userId")
public static void sendBroadcastNotification(String message, String icon, String severity) {
logger.debug("sending broadcast notification '{}' to all users", message);
if (cloudService != null) {
cloudService.sendBroadcastNotification(message, icon, severity);
}
}
}

View File

@@ -0,0 +1,629 @@
/**
* 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.io.openhabcloud.internal;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Request.FailureListener;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Response.ContentListener;
import org.eclipse.jetty.client.api.Response.HeadersListener;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.URIUtil;
import org.json.JSONException;
import org.json.JSONObject;
import org.openhab.core.OpenHAB;
import org.openhab.core.common.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.socket.client.IO;
import io.socket.client.Manager;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;
import io.socket.engineio.client.Transport;
/**
* This class provides communication between openHAB and the openHAB Cloud service.
* It also implements async http proxy for serving requests from user to
* openHAB through the openHAB Cloud. It uses Socket.IO connection to connect to
* the openHAB Cloud service and Jetty Http client to send local http requests to
* openHAB.
*
* @author Victor Belov - Initial contribution
* @author Kai Kreuzer - migrated code to new Jetty client and ESH APIs
*
*/
public class CloudClient {
/*
* Logger for this class
*/
private Logger logger = LoggerFactory.getLogger(CloudClient.class);
/*
* This variable holds base URL for the openHAB Cloud connections
*/
private final String baseURL;
/*
* This variable holds openHAB's UUID for authenticating and connecting to the openHAB Cloud
*/
private final String uuid;
/*
* This variable holds openHAB's secret for authenticating and connecting to the openHAB Cloud
*/
private final String secret;
/*
* This variable holds local openHAB's base URL for connecting to the local openHAB instance
*/
private final String localBaseUrl;
/*
* This variable holds instance of Jetty HTTP client to make requests to local openHAB
*/
private final HttpClient jettyClient;
/*
* This hashmap holds HTTP requests to local openHAB which are currently running
*/
private Map<Integer, Request> runningRequests;
/*
* This variable indicates if connection to the openHAB Cloud is currently in an established state
*/
private boolean isConnected;
/*
* This variable holds version of local openHAB
*/
private String openHABVersion;
/*
* This variable holds instance of Socket.IO client class which provides communication
* with the openHAB Cloud
*/
private Socket socket;
/*
* The protocol of the openHAB-cloud URL.
*/
private String protocol = "https";
/*
* This variable holds instance of CloudClientListener which provides callbacks to communicate
* certain events from the openHAB Cloud back to openHAB
*/
private CloudClientListener listener;
private boolean remoteAccessEnabled;
private Set<String> exposedItems;
/**
* Constructor of CloudClient
*
* @param uuid openHAB's UUID to connect to the openHAB Cloud
* @param secret openHAB's Secret to connect to the openHAB Cloud
* @param remoteAccessEnabled Allow the openHAB Cloud to be used as a remote proxy
* @param exposedItems Items that are made available to apps connected to the openHAB Cloud
*/
public CloudClient(HttpClient httpClient, String uuid, String secret, String baseURL, String localBaseUrl,
boolean remoteAccessEnabled, Set<String> exposedItems) {
this.uuid = uuid;
this.secret = secret;
this.baseURL = baseURL;
this.localBaseUrl = localBaseUrl;
this.remoteAccessEnabled = remoteAccessEnabled;
this.exposedItems = exposedItems;
runningRequests = new HashMap<>();
this.jettyClient = httpClient;
}
/**
* Connect to the openHAB Cloud
*/
public void connect() {
try {
socket = IO.socket(baseURL);
URL parsed = new URL(baseURL);
protocol = parsed.getProtocol();
} catch (URISyntaxException e) {
logger.error("Error creating Socket.IO: {}", e.getMessage());
} catch (MalformedURLException e) {
logger.error("Error parsing baseURL to get protocol, assuming https. Error: {}", e.getMessage());
}
socket.io().on(Manager.EVENT_TRANSPORT, new Emitter.Listener() {
@Override
public void call(Object... args) {
logger.trace("Manager.EVENT_TRANSPORT");
Transport transport = (Transport) args[0];
transport.on(Transport.EVENT_REQUEST_HEADERS, new Emitter.Listener() {
@Override
public void call(Object... args) {
logger.trace("Transport.EVENT_REQUEST_HEADERS");
@SuppressWarnings("unchecked")
Map<String, List<String>> headers = (Map<String, List<String>>) args[0];
headers.put("uuid", Arrays.asList(uuid));
headers.put("secret", Arrays.asList(secret));
headers.put("openhabversion", Arrays.asList(OpenHAB.getVersion()));
headers.put("clientversion", Arrays.asList(CloudService.clientVersion));
headers.put("remoteaccess", Arrays.asList(((Boolean) remoteAccessEnabled).toString()));
}
});
}
});
socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
@Override
public void call(Object... args) {
logger.debug("Socket.IO connected");
isConnected = true;
onConnect();
}
}).on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {
@Override
public void call(Object... args) {
logger.debug("Socket.IO disconnected");
isConnected = false;
onDisconnect();
}
}).on(Socket.EVENT_ERROR, new Emitter.Listener() {
@Override
public void call(Object... args) {
logger.error("Error connecting to the openHAB Cloud instance: {}", args[0]);
}
}).on("request", new Emitter.Listener() {
@Override
public void call(Object... args) {
onEvent("request", (JSONObject) args[0]);
}
}).on("cancel", new Emitter.Listener() {
@Override
public void call(Object... args) {
onEvent("cancel", (JSONObject) args[0]);
}
}).on("command", new Emitter.Listener() {
@Override
public void call(Object... args) {
onEvent("command", (JSONObject) args[0]);
}
});
socket.connect();
}
/**
* Callback method for socket.io client which is called when connection is established
*/
public void onConnect() {
logger.info("Connected to the openHAB Cloud service (UUID = {}, base URL = {})", this.uuid, this.localBaseUrl);
isConnected = true;
}
/**
* Callback method for socket.io client which is called when disconnect occurs
*/
public void onDisconnect() {
logger.info("Disconnected from the openHAB Cloud service (UUID = {}, base URL = {})", this.uuid,
this.localBaseUrl);
isConnected = false;
// And clean up the list of running requests
if (runningRequests != null) {
runningRequests.clear();
}
}
/**
* Callback method for socket.io client which is called when an error occurs
*/
public void onError(IOException error) {
logger.debug("{}", error.getMessage());
}
/**
* Callback method for socket.io client which is called when a message is received
*/
public void onEvent(String event, JSONObject data) {
logger.debug("on(): {}", event);
if ("command".equals(event)) {
handleCommandEvent(data);
return;
}
if (remoteAccessEnabled) {
if ("request".equals(event)) {
handleRequestEvent(data);
} else if ("cancel".equals(event)) {
handleCancelEvent(data);
} else {
logger.warn("Unsupported event from openHAB Cloud: {}", event);
}
}
}
private void handleRequestEvent(JSONObject data) {
try {
// Get unique request Id
int requestId = data.getInt("id");
logger.debug("Got request {}", requestId);
// Get request path
String requestPath = data.getString("path");
// Get request method
String requestMethod = data.getString("method");
// Get request body
String requestBody = data.getString("body");
// Get JSONObject for request headers
JSONObject requestHeadersJson = data.getJSONObject("headers");
logger.debug("{}", requestHeadersJson.toString());
// Get JSONObject for request query parameters
JSONObject requestQueryJson = data.getJSONObject("query");
// Create URI builder with base request URI of openHAB and path from request
String newPath = URIUtil.addPaths(localBaseUrl, requestPath);
@SuppressWarnings("unchecked")
Iterator<String> queryIterator = requestQueryJson.keys();
// Add query parameters to URI builder, if any
newPath += "?";
while (queryIterator.hasNext()) {
String queryName = queryIterator.next();
newPath += queryName;
newPath += "=";
newPath += URLEncoder.encode(requestQueryJson.getString(queryName), "UTF-8");
if (queryIterator.hasNext()) {
newPath += "&";
}
}
// Finally get the future request URI
URI requestUri = new URI(newPath);
// All preparations which are common for different methods are done
// Now perform the request to openHAB
// If method is GET
logger.debug("Request method is {}", requestMethod);
Request request = jettyClient.newRequest(requestUri);
setRequestHeaders(request, requestHeadersJson);
String proto = protocol;
if (data.has("protocol")) {
proto = data.getString("protocol");
}
request.header("X-Forwarded-Proto", proto);
if (requestMethod.equals("GET")) {
request.method(HttpMethod.GET);
} else if (requestMethod.equals("POST")) {
request.method(HttpMethod.POST);
request.content(new BytesContentProvider(requestBody.getBytes()));
} else if (requestMethod.equals("PUT")) {
request.method(HttpMethod.PUT);
request.content(new BytesContentProvider(requestBody.getBytes()));
} else {
// TODO: Reject unsupported methods
logger.warn("Unsupported request method {}", requestMethod);
return;
}
ResponseListener listener = new ResponseListener(requestId);
request.onResponseHeaders(listener).onResponseContent(listener).onRequestFailure(listener).send(listener);
// If successfully submitted request to http client, add it to the list of currently
// running requests to be able to cancel it if needed
runningRequests.put(requestId, request);
} catch (JSONException | IOException | URISyntaxException e) {
logger.debug("{}", e.getMessage());
}
}
private void setRequestHeaders(Request request, JSONObject requestHeadersJson) {
@SuppressWarnings("unchecked")
Iterator<String> headersIterator = requestHeadersJson.keys();
// Convert JSONObject of headers into Header ArrayList
while (headersIterator.hasNext()) {
String headerName = headersIterator.next();
String headerValue;
try {
headerValue = requestHeadersJson.getString(headerName);
logger.debug("Jetty set header {} = {}", headerName, headerValue);
if (!headerName.equalsIgnoreCase("Content-Length")) {
request.header(headerName, headerValue);
}
} catch (JSONException e) {
logger.warn("Error processing request headers: {}", e.getMessage());
}
}
}
private void handleCancelEvent(JSONObject data) {
try {
int requestId = data.getInt("id");
logger.debug("Received cancel for request {}", requestId);
// Find and abort running request
if (runningRequests.containsKey(requestId)) {
Request request = runningRequests.get(requestId);
request.abort(new InterruptedException());
runningRequests.remove(requestId);
}
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
}
private void handleCommandEvent(JSONObject data) {
String itemName = data.getString("item");
if (exposedItems.contains(itemName)) {
try {
logger.debug("Received command {} for item {}.", data.getString("command"), itemName);
if (this.listener != null) {
this.listener.sendCommand(itemName, data.getString("command"));
}
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
} else {
logger.warn("Received command from openHAB Cloud for item '{}', which is not exposed.", itemName);
}
}
/**
* This method sends notification to the openHAB Cloud
*
* @param userId openHAB Cloud user id
* @param message notification message text
* @param icon name of the icon for this notification
* @param severity severity name for this notification
*
*/
public void sendNotification(String userId, String message, String icon, String severity) {
if (isConnected()) {
JSONObject notificationMessage = new JSONObject();
try {
notificationMessage.put("userId", userId);
notificationMessage.put("message", message);
notificationMessage.put("icon", icon);
notificationMessage.put("severity", severity);
socket.emit("notification", notificationMessage);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
} else {
logger.debug("No connection, notification is not sent");
}
}
/**
* This method sends log notification to the openHAB Cloud
*
* @param message notification message text
* @param icon name of the icon for this notification
* @param severity severity name for this notification
*
*/
public void sendLogNotification(String message, String icon, String severity) {
if (isConnected()) {
JSONObject notificationMessage = new JSONObject();
try {
notificationMessage.put("message", message);
notificationMessage.put("icon", icon);
notificationMessage.put("severity", severity);
socket.emit("lognotification", notificationMessage);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
} else {
logger.debug("No connection, notification is not sent");
}
}
/**
* This method sends broadcast notification to the openHAB Cloud
*
* @param message notification message text
* @param icon name of the icon for this notification
* @param severity severity name for this notification
*
*/
public void sendBroadcastNotification(String message, String icon, String severity) {
if (isConnected()) {
JSONObject notificationMessage = new JSONObject();
try {
notificationMessage.put("message", message);
notificationMessage.put("icon", icon);
notificationMessage.put("severity", severity);
socket.emit("broadcastnotification", notificationMessage);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
} else {
logger.debug("No connection, notification is not sent");
}
}
/**
* Send item update to openHAB Cloud
*
* @param itemName the name of the item
* @param itemState updated item state
*
*/
public void sendItemUpdate(String itemName, String itemState) {
if (isConnected()) {
logger.debug("Sending update '{}' for item '{}'", itemState, itemName);
JSONObject itemUpdateMessage = new JSONObject();
try {
itemUpdateMessage.put("itemName", itemName);
itemUpdateMessage.put("itemStatus", itemState);
socket.emit("itemupdate", itemUpdateMessage);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
} else {
logger.debug("No connection, Item update is not sent");
}
}
/**
* Returns true if openHAB Cloud connection is active
*/
public boolean isConnected() {
return isConnected;
}
/**
* Disconnect from openHAB Cloud
*/
public void shutdown() {
logger.info("Shutting down openHAB Cloud service connection");
socket.disconnect();
}
public String getOpenHABVersion() {
return openHABVersion;
}
public void setOpenHABVersion(String openHABVersion) {
this.openHABVersion = openHABVersion;
}
public void setListener(CloudClientListener listener) {
this.listener = listener;
}
/*
* An internal class which forwards response headers and data back to the openHAB Cloud
*/
private class ResponseListener
implements Response.CompleteListener, HeadersListener, ContentListener, FailureListener {
private static final String THREADPOOL_OPENHABCLOUD = "openhabcloud";
private int mRequestId;
private boolean mHeadersSent = false;
public ResponseListener(int requestId) {
mRequestId = requestId;
}
private JSONObject getJSONHeaders(HttpFields httpFields) {
JSONObject headersJSON = new JSONObject();
try {
for (HttpField field : httpFields) {
headersJSON.put(field.getName(), field.getValue());
}
} catch (JSONException e) {
logger.warn("Error forming response headers: {}", e.getMessage());
}
return headersJSON;
}
@Override
public void onComplete(Result result) {
// Remove this request from list of running requests
runningRequests.remove(mRequestId);
if ((result != null && result.isFailed())
&& (result.getResponse() != null && result.getResponse().getStatus() != HttpStatus.OK_200)) {
if (result.getFailure() != null) {
logger.warn("Jetty request {} failed: {}", mRequestId, result.getFailure().getMessage());
}
if (result.getRequestFailure() != null) {
logger.warn("Request Failure: {}", result.getRequestFailure().getMessage());
}
if (result.getResponseFailure() != null) {
logger.warn("Response Failure: {}", result.getResponseFailure().getMessage());
}
}
/**
* What is this? In some cases where latency is very low the myopenhab service
* can receive responseFinished before the headers or content are received and I
* cannot find another workaround to prevent it.
*/
ThreadPoolManager.getScheduledPool(THREADPOOL_OPENHABCLOUD).schedule(() -> {
JSONObject responseJson = new JSONObject();
try {
responseJson.put("id", mRequestId);
socket.emit("responseFinished", responseJson);
logger.debug("Finished responding to request {}", mRequestId);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
}, 1, TimeUnit.MILLISECONDS);
}
@Override
public synchronized void onFailure(Request request, Throwable failure) {
JSONObject responseJson = new JSONObject();
try {
responseJson.put("id", mRequestId);
responseJson.put("responseStatusText", "openHAB connection error: " + failure.getMessage());
socket.emit("responseError", responseJson);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
}
@Override
public void onContent(Response response, ByteBuffer content) {
logger.debug("Jetty received response content of size {}", String.valueOf(content.remaining()));
JSONObject responseJson = new JSONObject();
try {
responseJson.put("id", mRequestId);
responseJson.put("body", BufferUtil.toArray(content));
socket.emit("responseContentBinary", responseJson);
logger.debug("Sent content to request {}", mRequestId);
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
}
@Override
public void onHeaders(Response response) {
if (!mHeadersSent) {
logger.debug("Jetty finished receiving response header");
JSONObject responseJson = new JSONObject();
mHeadersSent = true;
try {
responseJson.put("id", mRequestId);
responseJson.put("headers", getJSONHeaders(response.getHeaders()));
responseJson.put("responseStatusCode", response.getStatus());
responseJson.put("responseStatusText", "OK");
socket.emit("responseHeader", responseJson);
logger.debug("Sent headers to request {}", mRequestId);
logger.debug("{}", responseJson.toString());
} catch (JSONException e) {
logger.debug("{}", e.getMessage());
}
} else {
// We should not send headers for the second time...
}
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* 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.io.openhabcloud.internal;
/**
* This interface provides callbacks from CloudClient
*
* @author Victor Belov - Initial contribution
* @author Kai Kreuzer - migrated code to ESH APIs
*
*/
public interface CloudClientListener {
/**
* This method receives command for an item from the openHAB Cloud client and should post it
* into openHAB
*
* @param item the {@link String} containing item name
* @param command the {@link String} containing a command
*/
public void sendCommand(String item, String command);
}

View File

@@ -0,0 +1,397 @@
/**
* 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.io.openhabcloud.internal;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.OpenHAB;
import org.openhab.core.config.core.ConfigConstants;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventFilter;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.id.InstanceUUID;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.items.events.ItemStateEvent;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.model.script.engine.action.ActionService;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.types.Command;
import org.openhab.core.types.TypeParser;
import org.openhab.io.openhabcloud.NotificationAction;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
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.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class starts the cloud connection service and implements interface to communicate with the cloud.
*
* @author Victor Belov - Initial contribution
* @author Kai Kreuzer - migrated code to new Jetty client and ESH APIs
*/
@Component(immediate = true, service = { EventSubscriber.class,
ActionService.class }, configurationPid = "org.openhab.openhabcloud", property = {
Constants.SERVICE_PID + "=org.openhab.openhabcloud",
ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=io:openhabcloud",
ConfigurableService.SERVICE_PROPERTY_LABEL + "=openHAB Cloud",
ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=io" })
public class CloudService implements ActionService, CloudClientListener, EventSubscriber {
private static final String CFG_EXPOSE = "expose";
private static final String CFG_BASE_URL = "baseURL";
private static final String CFG_MODE = "mode";
private static final String SECRET_FILE_NAME = "openhabcloud" + File.separator + "secret";
private static final String DEFAULT_URL = "https://myopenhab.org/";
private static final int DEFAULT_LOCAL_OPENHAB_MAX_CONCURRENT_REQUESTS = 200;
private static final int DEFAULT_LOCAL_OPENHAB_REQUEST_TIMEOUT = 30000;
private static final String HTTPCLIENT_NAME = "openhabcloud";
private Logger logger = LoggerFactory.getLogger(CloudService.class);
public static String clientVersion = null;
private CloudClient cloudClient;
private String cloudBaseUrl = null;
private HttpClient httpClient;
protected ItemRegistry itemRegistry = null;
protected EventPublisher eventPublisher = null;
private boolean remoteAccessEnabled = true;
private Set<String> exposedItems = null;
private int localPort;
public CloudService() {
}
/**
* This method sends notification message to mobile app through the openHAB Cloud service
*
* @param userId the {@link String} containing the openHAB Cloud user id to send message to
* @param message the {@link String} containing a message to send to specified user id
* @param icon the {@link String} containing a name of the icon to be used with this notification
* @param severity the {@link String} containing severity (good, info, warning, error) of notification
*/
public void sendNotification(String userId, String message, String icon, String severity) {
logger.debug("Sending message '{}' to user id {}", message, userId);
cloudClient.sendNotification(userId, message, icon, severity);
}
/**
* Sends an advanced notification to log. Log notifications are not pushed to user
* devices but are shown to all account users in notifications log
*
* @param message the {@link String} containing a message to send to specified user id
* @param icon the {@link String} containing a name of the icon to be used with this notification
* @param severity the {@link String} containing severity (good, info, warning, error) of notification
*/
public void sendLogNotification(String message, String icon, String severity) {
logger.debug("Sending log message '{}'", message);
cloudClient.sendLogNotification(message, icon, severity);
}
/**
* Sends a broadcast notification. Broadcast notifications are pushed to all
* mobile devices of all users of the account
*
* @param message the {@link String} containing a message to send to specified user id
* @param icon the {@link String} containing a name of the icon to be used with this notification
* @param severity the {@link String} containing severity (good, info, warning, error) of notification
*/
public void sendBroadcastNotification(String message, String icon, String severity) {
logger.debug("Sending broadcast message '{}' to all users", message);
cloudClient.sendBroadcastNotification(message, icon, severity);
}
@Activate
protected void activate(BundleContext context, Map<String, ?> config) {
clientVersion = StringUtils.substringBefore(context.getBundle().getVersion().toString(), ".qualifier");
localPort = HttpServiceUtil.getHttpServicePort(context);
if (localPort == -1) {
logger.warn("openHAB Cloud connector not started, since no local HTTP port could be determined");
} else {
logger.debug("openHAB Cloud connector activated");
checkJavaVersion();
modified(config);
}
}
private void checkJavaVersion() {
String version = System.getProperty("java.version");
if (version.charAt(2) == '8') {
// we are on Java 8, let's check the update
String update = version.substring(version.indexOf('_') + 1);
try {
Integer uVersion = Integer.valueOf(update);
if (uVersion < 101) {
logger.warn(
"You are running Java {} - the openhab Cloud connection requires at least Java 1.8.0_101, if your cloud server uses Let's Encrypt certificates!",
version);
}
} catch (NumberFormatException e) {
logger.debug("Could not determine update version of java {}", version);
}
}
}
@Deactivate
protected void deactivate() {
logger.debug("openHAB Cloud connector deactivated");
cloudClient.shutdown();
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("Could not stop Jetty http client", e);
}
}
@Modified
protected void modified(Map<String, ?> config) {
if (config != null && config.get(CFG_MODE) != null) {
remoteAccessEnabled = "remote".equals(config.get(CFG_MODE));
} else {
logger.debug("remoteAccessEnabled is not set, keeping value '{}'", remoteAccessEnabled);
}
if (config.get(CFG_BASE_URL) != null) {
cloudBaseUrl = (String) config.get(CFG_BASE_URL);
} else {
cloudBaseUrl = DEFAULT_URL;
}
exposedItems = new HashSet<>();
Object expCfg = config.get(CFG_EXPOSE);
if (expCfg instanceof String) {
String value = (String) expCfg;
while (value.startsWith("[")) {
value = value.substring(1);
}
while (value.endsWith("]")) {
value = value.substring(0, value.length() - 1);
}
for (String itemName : Arrays.asList((value).split(","))) {
exposedItems.add(itemName.trim());
}
} else if (expCfg instanceof Iterable) {
for (Object entry : ((Iterable<?>) expCfg)) {
exposedItems.add(entry.toString());
}
}
logger.debug("UUID = {}, secret = {}", InstanceUUID.get(), getSecret());
if (cloudClient != null) {
cloudClient.shutdown();
}
httpClient.setMaxConnectionsPerDestination(DEFAULT_LOCAL_OPENHAB_MAX_CONCURRENT_REQUESTS);
httpClient.setConnectTimeout(DEFAULT_LOCAL_OPENHAB_REQUEST_TIMEOUT);
httpClient.setFollowRedirects(false);
if (!httpClient.isRunning()) {
try {
httpClient.start();
} catch (Exception e) {
logger.error("Could not start Jetty http client", e);
}
}
String localBaseUrl = "http://localhost:" + localPort;
cloudClient = new CloudClient(httpClient, InstanceUUID.get(), getSecret(), cloudBaseUrl, localBaseUrl,
remoteAccessEnabled, exposedItems);
cloudClient.setOpenHABVersion(OpenHAB.getVersion());
cloudClient.connect();
cloudClient.setListener(this);
NotificationAction.cloudService = this;
}
@Override
public String getActionClassName() {
return NotificationAction.class.getCanonicalName();
}
@Override
public Class<?> getActionClass() {
return NotificationAction.class;
}
/**
* Reads the first line from specified file
*/
private String readFirstLine(File file) {
List<String> lines = null;
try (InputStream fis = new FileInputStream(file)) {
lines = IOUtils.readLines(fis);
} catch (IOException ioe) {
// no exception handling - we just return the empty String
}
return lines != null && !lines.isEmpty() ? lines.get(0) : "";
}
/**
* Writes a String to a specified file
*/
private void writeFile(File file, String content) {
// create intermediary directories
file.getParentFile().mkdirs();
try (OutputStream fos = new FileOutputStream(file)) {
IOUtils.write(content, fos);
logger.debug("Created file '{}' with content '{}'", file.getAbsolutePath(), content);
} catch (FileNotFoundException e) {
logger.error("Couldn't create file '{}'.", file.getPath(), e);
} catch (IOException e) {
logger.error("Couldn't write to file '{}'.", file.getPath(), e);
}
}
/**
* Creates a random secret and writes it to the <code>userdata/openhabcloud</code>
* directory. An existing <code>secret</code> file won't be overwritten.
* Returns either existing secret from the file or newly created secret.
*/
private String getSecret() {
File file = new File(ConfigConstants.getUserDataFolder() + File.separator + SECRET_FILE_NAME);
String newSecretString = "";
if (!file.exists()) {
newSecretString = RandomStringUtils.randomAlphanumeric(20);
logger.debug("New secret = {}", newSecretString);
writeFile(file, newSecretString);
} else {
newSecretString = readFirstLine(file);
logger.debug("Using secret at '{}' with content '{}'", file.getAbsolutePath(), newSecretString);
}
return newSecretString;
}
@Override
public void sendCommand(String itemName, String commandString) {
try {
if (itemRegistry != null) {
Item item = itemRegistry.getItem(itemName);
Command command = null;
if (item != null) {
if (this.eventPublisher != null) {
if ("toggle".equalsIgnoreCase(commandString)
&& (item instanceof SwitchItem || item instanceof RollershutterItem)) {
if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) {
command = OnOffType.OFF;
}
if (OnOffType.OFF.equals(item.getStateAs(OnOffType.class))) {
command = OnOffType.ON;
}
if (UpDownType.UP.equals(item.getStateAs(UpDownType.class))) {
command = UpDownType.DOWN;
}
if (UpDownType.DOWN.equals(item.getStateAs(UpDownType.class))) {
command = UpDownType.UP;
}
} else {
command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString);
}
if (command != null) {
logger.debug("Received command '{}' for item '{}'", commandString, itemName);
this.eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command));
} else {
logger.warn("Received invalid command '{}' for item '{}'", commandString, itemName);
}
}
} else {
logger.warn("Received command '{}' for non-existent item '{}'", commandString, itemName);
}
} else {
return;
}
} catch (ItemNotFoundException e) {
logger.warn("Received command for a non-existent item '{}'", itemName);
}
}
@Reference
protected void setHttpClientFactory(HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.createHttpClient(HTTPCLIENT_NAME);
this.httpClient.setStopTimeout(0);
}
protected void unsetHttpClientFactory(HttpClientFactory httpClientFactory) {
this.httpClient = null;
}
@Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC)
public void setItemRegistry(ItemRegistry itemRegistry) {
this.itemRegistry = itemRegistry;
}
public void unsetItemRegistry(ItemRegistry itemRegistry) {
this.itemRegistry = null;
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
public void setEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void unsetEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = null;
}
@Override
public Set<String> getSubscribedEventTypes() {
return Collections.singleton(ItemStateEvent.TYPE);
}
@Override
public EventFilter getEventFilter() {
return null;
}
@Override
public void receive(Event event) {
ItemStateEvent ise = (ItemStateEvent) event;
if (exposedItems != null && exposedItems.contains(ise.getItemName())) {
cloudClient.sendItemUpdate(ise.getItemName(), ise.getItemState().toString());
}
}
}

View File

@@ -0,0 +1,28 @@
<?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="io:openhabcloud">
<parameter name="mode" type="text" required="true">
<label>Mode</label>
<description>What features of the openHAB Cloud service should be used.</description>
<options>
<option value="notification">Notifications</option>
<option value="remote">Notifications &amp; Remote Access</option>
</options>
<default>remote</default>
</parameter>
<parameter name="expose" type="text" required="false" multiple="true">
<label>Items to Expose</label>
<description>List of items that are made accessible to IFTTT and similar services.</description>
<context>item</context>
</parameter>
<parameter name="baseURL" type="text" required="false">
<label>Base URL</label>
<description>Base URL for the openHAB Cloud server</description>
<default>https://myopenhab.org/</default>
</parameter>
</config-description>
</config-description:config-descriptions>