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,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.io.openhabcloud</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,40 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
engine.io-client
* License: MIT License
* Project: https://socketio.github.io/engine.io-client-java/
* Source: https://github.com/socketio/engine.io-client-java
json
* License: JSON License
* Project: https://github.com/stleary/JSON-java
* Source: https://github.com/stleary/JSON-java
okhttp
* License: Apache License 2.0
* Project: https://square.github.io/okhttp/
* Source: https://github.com/square/okhttp
okio
* License: Apache License 2.0
* Project: https://github.com/square/okio
* Source: https://github.com/square/okio
socket.io-client
* License: MIT License
* Project: https://socketio.github.io/socket.io-client-java/
* Source: https://github.com/socketio/socket.io-client-java

View File

@@ -0,0 +1,67 @@
# openHAB Cloud Connector
The openHAB Cloud Connector allows connecting the local openHAB runtime to a remote [openHAB Cloud](https://github.com/openhab/openhab-cloud/blob/master/README.md) instance, such as [myopenHAB.org](https://www.myopenHAB.org), which is an instance of the
openHAB Cloud service hosted by the [openHAB Foundation](https://www.openhabfoundation.org/).
## Features
The openHAB Cloud service (and thus the connector to it) is useful for different use cases:
* It allows remote access to local openHAB instances without having to expose ports to the Internet or to require a complex VPN setup.
* It serves as a connector to Google Cloud Messaging (GCM) and Apple Push Notifications (APN) for pushing notifications to mobile phone apps.
* It brings integration possibilities with services that require an OAuth2 authentication against a web server, such as IFTTT or Amazon Alexa Skills.
## UUID and Secret
To authenticate with the openHAB Cloud service the add-on generates two values when the add-on is installed.
These values need to be entered in your account settings of the openHAB Cloud service.
The first one is a unique identifier, which allows to identify your runtime.
One can think of it as something similar like a username for the cloud authentication.
The second one is a random secret key which serves as a password.
Both values are written to the local file system.
If you loose these files for some reason, openHAB will automatically generate new ones.
You will then have to reconfigure UUID and secret in the openHAB Cloud service under the _My account_ section.
Location of UUID and Secret:
| File | Regular Installation | APT Installation |
|--------|------------------------------|---------------------------------------|
| UUID | userdata/uuid | /var/lib/openhab2/uuid |
| Secret | userdata/openhabcloud/secret | /var/lib/openhab2/openhabcloud/secret |
## Configuration
After installing this add-on, you will find configuration options in the Paper UI under _Configuration->Services->IO->openHAB Cloud_:
![Configuration](contrib/doc/cfg.png)
Please note, that you should not expose all your items in this settings dialog.
Also note that at present this feature is currently turned off.
You just need to expose those items, which you want to be accessible by IFTTT.
This setting has no affect on Alexa or Google Assistant functionality.
Alternatively, you can configure the settings in the file `conf/services/openhabcloud.cfg`:
```
############################## openHAB Cloud Connector #############################
# The URL of the openHAB Cloud service to connect to.
# Optional, default is set to the service offered by the openHAB Foundation
# (https://myopenhab.org/)
#baseURL=
# Defines the mode in which you want to operate the connector.
# Possible values are:
# - notification: Only push notifications are enabled, no remote access is allowed.
# - remote: Push notifications and remote access are enabled.
# Optional, default is 'remote'.
#mode=
# A comma-separated list of items to be exposed to external services like IFTTT.
# Events of those items are pushed to the openHAB Cloud and commands received for
# these items from the openHAB Cloud service are accepted and sent to the local bus.
# Optional, default is an empty list.
#expose=
```
Note: The exposed items will show up after they receive an update to their state.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.io.openhabcloud</artifactId>
<name>openHAB Add-ons :: Bundles :: IO :: openHAB Cloud Connector</name>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20180813</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.servicemix.bundles</groupId>
<artifactId>org.apache.servicemix.bundles.okhttp</artifactId>
<version>3.8.1_1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.servicemix.bundles</groupId>
<artifactId>org.apache.servicemix.bundles.okio</artifactId>
<version>1.13.0_1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>io.socket.socket.io-client</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>io.socket.engine.io-client</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

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>