added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.exec/.classpath
Normal file
32
bundles/org.openhab.binding.exec/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.binding.exec/.project
Normal file
23
bundles/org.openhab.binding.exec/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.exec</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>
|
||||
13
bundles/org.openhab.binding.exec/NOTICE
Normal file
13
bundles/org.openhab.binding.exec/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
184
bundles/org.openhab.binding.exec/README.md
Normal file
184
bundles/org.openhab.binding.exec/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Exec Binding
|
||||
|
||||
This binding integrates the possibility to execute arbitrary shell commands.
|
||||
|
||||
## Supported Things
|
||||
|
||||
Currently, the binding supports a single type of Thing, being the `command` Thing.
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
For security reasons all commands need to be whitelisted.
|
||||
Allowed commands need to be added to the `misc/exec.whitelist` file in the configuration directory.
|
||||
Every command needs to be on a separate line.
|
||||
|
||||
Example:
|
||||
|
||||
```shell
|
||||
/bin/echo "Hello world!"
|
||||
/usr/local/bin/apcaccess status
|
||||
php ./configurations/scripts/script.php %2$s
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
Note that the commands are executed in the context and with the privileges of the process running the Java Virtual Machine.
|
||||
On a Linux system the system user `openhab` needs to have the privileges needed to execute your intended command.
|
||||
It is advised to test the correct operation of the command in the scope of the `openhab` user on the command line first:
|
||||
|
||||
```shell
|
||||
sudo -u openhab <YOUR COMMAND>
|
||||
```
|
||||
|
||||
It is not advised to run the virtual machine as superuser/root.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The "command" Thing requires the command to execute on the shell.
|
||||
Optionally one can specify:
|
||||
|
||||
- `transform` - A [transformation](https://www.openhab.org/docs/configuration/transformations.html) to apply on the execution result string.
|
||||
- `interval` - An interval, in seconds, the command will be repeatedly executed. Default is 60 seconds, set to 0 to avoid automatic repetition.
|
||||
- `timeout` - A time-out, in seconds, the execution of the command will time out, and lastly,
|
||||
- `autorun` - A boolean parameter to make the command execute immediately every time the input channel is sent a different openHAB command. If choosing autorun, you may wish to also set `interval=0`. Note that sending the same command a second time will not trigger execution.
|
||||
|
||||
For each shell command, a separate Thing has to be defined.
|
||||
|
||||
```java
|
||||
Thing exec:command:uniquename [command="/command/to/execute here", interval=15, timeout=5, autorun=false]
|
||||
```
|
||||
|
||||
The `command` itself can be enhanced using the well known syntax of the [Java formatter class syntax](https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html#syntax).
|
||||
The following parameters are automatically added:
|
||||
|
||||
- the current date (as java.util.Date, example: `%1$tY-%1$tm-%1$td`)
|
||||
- the current (or last) command to the input channel (see below, example: `%2$s`)
|
||||
|
||||
note - if you trigger execution using interval or the run channel, the `%2` substitution will use the most recent command (if there has been one) sent to the input channel. The state of the Item linked to input channel is ignored.
|
||||
|
||||
## Channels
|
||||
|
||||
All Things support the following channels:
|
||||
|
||||
| Channel Type ID | Item Type | Description |
|
||||
|-----------------|-----------|--------------------------------------------------------------------------------------|
|
||||
| input | String | Input parameter to provide to the command |
|
||||
| output | String | Output of the last execution of the command |
|
||||
| exit | Number | The exit value of the last execution of the command |
|
||||
| run | Switch | Send ON to execute the command, the current state tells whether it is running or not |
|
||||
| lastexecution | DateTime | Time/Date the command was last executed, in yyyy-MM-dd'T'HH:mm:ss.SSSZ format |
|
||||
|
||||
|
||||
## Minimal Example
|
||||
|
||||
**demo.things**
|
||||
|
||||
```java
|
||||
Thing exec:command:apc [command="/usr/local/bin/apcaccess status", interval=15, timeout=5]
|
||||
Thing exec:command:myscript [command="php ./configurations/scripts/script.php %2$s", transform="REGEX((.*?))"]
|
||||
```
|
||||
|
||||
**demo.items**
|
||||
|
||||
```java
|
||||
String APCRaw "[%s]" (All) {channel="exec:command:apc:output"}
|
||||
Switch APCRunning {channel="exec:command:apc:run"}
|
||||
Number APCExitValue "[%d]" {channel="exec:command:apc:exit"}
|
||||
DateTime APCLastExecution {channel="exec:command:apc:lastexecution"}
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
Following is an example how to set up an exec command thing, pass it a parameter, debug it with a rule and set the returned string to an Number Item.
|
||||
|
||||
**demo.things**
|
||||
|
||||
```java
|
||||
// "%2$s" will be replace by the input channel command, this makes it possible to use one command line with different arguments.
|
||||
// e.g: "ls" as <YOUR COMMAND> and "-a" or "-l" as additional argument sent to the input channel in the rule.
|
||||
Thing exec:command:yourcommand [ command="<YOUR COMMAND> %2$s", interval=0, autorun=false ]
|
||||
```
|
||||
|
||||
**demo.items**
|
||||
|
||||
```java
|
||||
Switch YourTrigger "External trigger [%s]"
|
||||
Number YourNumber "Your Number [%.1f °C]"
|
||||
|
||||
// state of the execution, is running or finished
|
||||
Switch yourcommand_Run {channel="exec:command:yourcommand:run", autoupdate="false"}
|
||||
// Arguments to be placed for '%2$s' in command line
|
||||
String yourcommand_Args {channel="exec:command:yourcommand:input"}
|
||||
// Output of command line execution
|
||||
String yourcommand_Out {channel="exec:command:yourcommand:output"}
|
||||
```
|
||||
|
||||
**demo.sitemap**
|
||||
|
||||
```java
|
||||
// Name of file and name of sitemap has to be the same
|
||||
sitemap demo label="Your Value"
|
||||
{
|
||||
Frame {
|
||||
Switch item=YourTrigger
|
||||
Text item=YourNumber
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**demo.rules**
|
||||
|
||||
```java
|
||||
rule "Set up your parameters"
|
||||
when
|
||||
Item YourTrigger changed
|
||||
then
|
||||
// here we can take different actions according to source Item
|
||||
if(YourTrigger.state == ON){
|
||||
yourcommand_Args.sendCommand("Additional Argument to command line for ON")
|
||||
}else{
|
||||
yourcommand_Args.sendCommand("Different Argument to command line for OFF")
|
||||
}
|
||||
// Caution : openHAB bus is asynchronous
|
||||
// we must let the command work before triggering execution (if autorun false)
|
||||
end
|
||||
|
||||
rule "begin your execution"
|
||||
when
|
||||
Item yourcommand_Args received command
|
||||
then
|
||||
// Separately triggering RUN allows subsequent executions with unchanged parameter %2
|
||||
// which autorun does not.
|
||||
if (yourcommand_Run.state != ON) {
|
||||
yourcommand_Run.sendCommand(ON)
|
||||
// the Run indicator state will go ON shortly, and return OFF when script finished
|
||||
}else{
|
||||
logInfo("Your command exec", "Script already in use, skipping execution.")
|
||||
}
|
||||
end
|
||||
|
||||
rule "script complete"
|
||||
when
|
||||
Item yourcommand_Run changed from ON to OFF
|
||||
then
|
||||
// Logging of ending
|
||||
logInfo("Your command exec", "Script has completed.")
|
||||
// Caution : openHAB bus is asynchronous
|
||||
// there is no guarantee the output Item will get updated before the run channel triggers rules
|
||||
end
|
||||
|
||||
rule "process your results"
|
||||
when
|
||||
Item yourcommand_Out received update
|
||||
then
|
||||
// Logging of raw command line result
|
||||
logInfo("Your command exec", "Raw result:" + yourcommand_Out.state )
|
||||
// If the returned string is just a number it can be parsed
|
||||
// If not, a regex or another transformation could be used
|
||||
YourNumber.postUpdate(Integer::parseInt(yourcommand_Out.state.toString) as Number)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
[openHAB community thread with a detailed example.](https://community.openhab.org/t/1-openhab-433mhz-radio-transmitter-tutorial/34977)
|
||||
17
bundles/org.openhab.binding.exec/pom.xml
Normal file
17
bundles/org.openhab.binding.exec/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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.binding.exec</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Exec Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.exec-${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-exec" description="Exec Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<configfile finalname="${openhab.conf}/misc/exec.whitelist" override="false">mvn:${project.groupId}/openhab-addons-external/${project.version}/cfg/exec.whitelist</configfile>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.exec/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -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.exec.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link ExecBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Karel Goderis - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ExecBindingConstants {
|
||||
public static final String BINDING_ID = "exec";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_COMMAND = new ThingTypeUID(BINDING_ID, "command");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String OUTPUT = "output";
|
||||
public static final String INPUT = "input";
|
||||
public static final String EXIT = "exit";
|
||||
public static final String RUN = "run";
|
||||
public static final String LAST_EXECUTION = "lastexecution";
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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.exec.internal;
|
||||
|
||||
import static org.openhab.binding.exec.internal.ExecBindingConstants.THING_COMMAND;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.exec.internal.handler.ExecHandler;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
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.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ExecHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Karel Goderis - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.exec")
|
||||
public class ExecHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_COMMAND);
|
||||
private final Logger logger = LoggerFactory.getLogger(ExecHandlerFactory.class);
|
||||
private final ExecWhitelistWatchService execWhitelistWatchService;
|
||||
|
||||
@Activate
|
||||
public ExecHandlerFactory(@Reference ExecWhitelistWatchService execWhitelistWatchService) {
|
||||
this.execWhitelistWatchService = execWhitelistWatchService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_COMMAND)) {
|
||||
return new ExecHandler(thing, execWhitelistWatchService);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.exec.internal;
|
||||
|
||||
import static java.nio.file.StandardWatchEventKinds.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.WatchEvent;
|
||||
import java.nio.file.WatchEvent.Kind;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.ConfigConstants;
|
||||
import org.openhab.core.service.AbstractWatchService;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ExecWhitelistWatchService} provides a whitelist check for exec commands
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@Component(service = ExecWhitelistWatchService.class)
|
||||
@NonNullByDefault
|
||||
public class ExecWhitelistWatchService extends AbstractWatchService {
|
||||
private static final String COMMAND_WHITELIST_PATH = ConfigConstants.getConfigFolder() + File.separator + "misc";
|
||||
private static final String COMMAND_WHITELIST_FILE = "exec.whitelist";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ExecWhitelistWatchService.class);
|
||||
private final Set<String> commandWhitelist = new HashSet<>();
|
||||
|
||||
@Activate
|
||||
public ExecWhitelistWatchService() {
|
||||
super(COMMAND_WHITELIST_PATH);
|
||||
processWatchEvent(null, null, Paths.get(COMMAND_WHITELIST_PATH, COMMAND_WHITELIST_FILE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean watchSubDirectories() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Kind<?>[] getWatchEventKinds(@Nullable Path directory) {
|
||||
return new Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processWatchEvent(@Nullable WatchEvent<?> event, @Nullable Kind<?> kind, @Nullable Path path) {
|
||||
if (path != null && path.endsWith(COMMAND_WHITELIST_FILE)) {
|
||||
commandWhitelist.clear();
|
||||
try {
|
||||
Files.lines(path).filter(line -> !line.trim().startsWith("#")).forEach(commandWhitelist::add);
|
||||
logger.debug("Updated command whitelist: {}", commandWhitelist);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Cannot read whitelist file, exec binding commands won't be processed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command is whitelisted
|
||||
*
|
||||
* @param command the command to check alias
|
||||
* @return true if whitelisted, false if not
|
||||
*/
|
||||
public boolean isWhitelisted(String command) {
|
||||
return commandWhitelist.contains(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* 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.exec.internal.handler;
|
||||
|
||||
import static org.openhab.binding.exec.internal.ExecBindingConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.exec.internal.ExecWhitelistWatchService;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.transform.TransformationException;
|
||||
import org.openhab.core.transform.TransformationHelper;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.FrameworkUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ExecHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Karel Goderis - Initial contribution
|
||||
* @author Constantin Piber - Added better argument support (delimiter and pass to shell)
|
||||
* @author Jan N. Klug - Add command whitelist check
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ExecHandler extends BaseThingHandler {
|
||||
/**
|
||||
* Use this to separate between command and parameter, and also between parameters.
|
||||
*/
|
||||
public static final String CMD_LINE_DELIMITER = "@@";
|
||||
|
||||
/**
|
||||
* Shell executables
|
||||
*/
|
||||
public static final String[] SHELL_WINDOWS = new String[] { "cmd" };
|
||||
public static final String[] SHELL_NIX = new String[] { "sh", "bash", "zsh", "csh" };
|
||||
private final ExecWhitelistWatchService execWhitelistWatchService;
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(ExecHandler.class);
|
||||
|
||||
private final BundleContext bundleContext;
|
||||
|
||||
// List of Configurations constants
|
||||
public static final String INTERVAL = "interval";
|
||||
public static final String TIME_OUT = "timeout";
|
||||
public static final String COMMAND = "command";
|
||||
public static final String TRANSFORM = "transform";
|
||||
public static final String AUTORUN = "autorun";
|
||||
|
||||
// RegEx to extract a parse a function String <code>'(.*?)\((.*)\)'</code>
|
||||
private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\)");
|
||||
|
||||
private @Nullable ScheduledFuture<?> executionJob;
|
||||
private @Nullable String lastInput;
|
||||
|
||||
private static Runtime rt = Runtime.getRuntime();
|
||||
|
||||
public ExecHandler(Thing thing, ExecWhitelistWatchService execWhitelistWatchService) {
|
||||
super(thing);
|
||||
this.bundleContext = FrameworkUtil.getBundle(ExecHandler.class).getBundleContext();
|
||||
this.execWhitelistWatchService = execWhitelistWatchService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
// Placeholder for later refinement
|
||||
} else {
|
||||
if (channelUID.getId().equals(RUN)) {
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
scheduler.schedule(this::execute, 0, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
} else if (channelUID.getId().equals(INPUT)) {
|
||||
if (command instanceof StringType) {
|
||||
String previousInput = lastInput;
|
||||
lastInput = command.toString();
|
||||
if (lastInput != null && !lastInput.equals(previousInput)) {
|
||||
if (getConfig().get(AUTORUN) != null && ((Boolean) getConfig().get(AUTORUN))) {
|
||||
logger.trace("Executing command '{}' after a change of the input channel to '{}'",
|
||||
getConfig().get(COMMAND), lastInput);
|
||||
scheduler.schedule(this::execute, 0, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
if (executionJob == null || executionJob.isCancelled()) {
|
||||
if ((getConfig().get(INTERVAL)) != null && ((BigDecimal) getConfig().get(INTERVAL)).intValue() > 0) {
|
||||
int pollingInterval = ((BigDecimal) getConfig().get(INTERVAL)).intValue();
|
||||
executionJob = scheduler.scheduleWithFixedDelay(this::execute, 0, pollingInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (executionJob != null && !executionJob.isCancelled()) {
|
||||
executionJob.cancel(true);
|
||||
executionJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
String commandLine = (String) getConfig().get(COMMAND);
|
||||
if (!execWhitelistWatchService.isWhitelisted(commandLine)) {
|
||||
logger.warn("Tried to execute '{}', but it is not contained in whitelist.", commandLine);
|
||||
return;
|
||||
}
|
||||
|
||||
int timeOut = 60000;
|
||||
if ((getConfig().get(TIME_OUT)) != null) {
|
||||
timeOut = ((BigDecimal) getConfig().get(TIME_OUT)).intValue() * 1000;
|
||||
}
|
||||
|
||||
if (commandLine != null && !commandLine.isEmpty()) {
|
||||
updateState(RUN, OnOffType.ON);
|
||||
|
||||
// For some obscure reason, when using Apache Common Exec, or using a straight implementation of
|
||||
// Runtime.Exec(), on Mac OS X (Yosemite and El Capitan), there seems to be a lock race condition
|
||||
// randomly appearing (on UNIXProcess) *when* one tries to gobble up the stdout and sterr output of the
|
||||
// subprocess in separate threads. It seems to be common "wisdom" to do that in separate threads, but
|
||||
// only when keeping everything between .exec() and .waitfor() in the same thread, this lock race
|
||||
// condition seems to go away. This approach of not reading the outputs in separate threads *might* be a
|
||||
// problem for external commands that generate a lot of output, but this will be dependent on the limits
|
||||
// of the underlying operating system.
|
||||
|
||||
try {
|
||||
if (lastInput != null) {
|
||||
commandLine = String.format(commandLine, Calendar.getInstance().getTime(), lastInput);
|
||||
} else {
|
||||
commandLine = String.format(commandLine, Calendar.getInstance().getTime());
|
||||
}
|
||||
} catch (IllegalFormatException e) {
|
||||
logger.warn(
|
||||
"An exception occurred while formatting the command line with the current time and input values : '{}'",
|
||||
e.getMessage());
|
||||
updateState(RUN, OnOffType.OFF);
|
||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
String[] cmdArray;
|
||||
String[] shell;
|
||||
if (commandLine.contains(CMD_LINE_DELIMITER)) {
|
||||
logger.debug("Splitting by '{}'", CMD_LINE_DELIMITER);
|
||||
try {
|
||||
cmdArray = commandLine.split(CMD_LINE_DELIMITER);
|
||||
} catch (PatternSyntaxException e) {
|
||||
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
|
||||
updateState(RUN, OnOffType.OFF);
|
||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Invoke shell with 'c' option and pass string
|
||||
logger.debug("Passing to shell for parsing command.");
|
||||
switch (getOperatingSystemType()) {
|
||||
case WINDOWS:
|
||||
shell = SHELL_WINDOWS;
|
||||
logger.debug("OS: WINDOWS ({})", getOperatingSystemName());
|
||||
cmdArray = createCmdArray(shell, "/c", commandLine);
|
||||
break;
|
||||
case LINUX:
|
||||
case MAC:
|
||||
case SOLARIS:
|
||||
// assume sh is present, should all be POSIX-compliant
|
||||
shell = SHELL_NIX;
|
||||
logger.debug("OS: *NIX ({})", getOperatingSystemName());
|
||||
cmdArray = createCmdArray(shell, "-c", commandLine);
|
||||
break;
|
||||
default:
|
||||
logger.debug("OS: Unknown ({})", getOperatingSystemName());
|
||||
logger.warn("OS {} not supported, please manually split commands!", getOperatingSystemName());
|
||||
updateState(RUN, OnOffType.OFF);
|
||||
updateState(OUTPUT, new StringType("OS not supported, please manually split commands!"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdArray.length == 0) {
|
||||
logger.trace("Empty command received, not executing");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace("The command to be executed will be '{}'", Arrays.asList(cmdArray));
|
||||
|
||||
Process proc;
|
||||
try {
|
||||
proc = rt.exec(cmdArray);
|
||||
} catch (Exception e) {
|
||||
logger.warn("An exception occurred while executing '{}' : '{}'", Arrays.asList(cmdArray),
|
||||
e.getMessage());
|
||||
updateState(RUN, OnOffType.OFF);
|
||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder outputBuilder = new StringBuilder();
|
||||
StringBuilder errorBuilder = new StringBuilder();
|
||||
|
||||
try (InputStreamReader isr = new InputStreamReader(proc.getInputStream());
|
||||
BufferedReader br = new BufferedReader(isr)) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
outputBuilder.append(line).append("\n");
|
||||
logger.debug("Exec [{}]: '{}'", "OUTPUT", line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("An exception occurred while reading the stdout when executing '{}' : '{}'", commandLine,
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
try (InputStreamReader isr = new InputStreamReader(proc.getErrorStream());
|
||||
BufferedReader br = new BufferedReader(isr)) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
errorBuilder.append(line).append("\n");
|
||||
logger.debug("Exec [{}]: '{}'", "ERROR", line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("An exception occurred while reading the stderr when executing '{}' : '{}'", commandLine,
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
boolean exitVal = false;
|
||||
try {
|
||||
exitVal = proc.waitFor(timeOut, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("An exception occurred while waiting for the process ('{}') to finish : '{}'", commandLine,
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
if (!exitVal) {
|
||||
logger.warn("Forcibly termininating the process ('{}') after a timeout of {} ms", commandLine, timeOut);
|
||||
proc.destroyForcibly();
|
||||
}
|
||||
|
||||
updateState(RUN, OnOffType.OFF);
|
||||
updateState(EXIT, new DecimalType(proc.exitValue()));
|
||||
|
||||
outputBuilder.append(errorBuilder.toString());
|
||||
|
||||
outputBuilder.append(errorBuilder.toString());
|
||||
|
||||
String transformedResponse = StringUtils.chomp(outputBuilder.toString());
|
||||
String transformation = (String) getConfig().get(TRANSFORM);
|
||||
|
||||
if (transformation != null && transformation.length() > 0) {
|
||||
transformedResponse = transformResponse(transformedResponse, transformation);
|
||||
}
|
||||
|
||||
updateState(OUTPUT, new StringType(transformedResponse));
|
||||
|
||||
DateTimeType stampType = new DateTimeType(ZonedDateTime.now());
|
||||
updateState(LAST_EXECUTION, stampType);
|
||||
}
|
||||
}
|
||||
|
||||
protected @Nullable String transformResponse(String response, String transformation) {
|
||||
String transformedResponse;
|
||||
|
||||
try {
|
||||
String[] parts = splitTransformationConfig(transformation);
|
||||
String transformationType = parts[0];
|
||||
String transformationFunction = parts[1];
|
||||
|
||||
TransformationService transformationService = TransformationHelper.getTransformationService(bundleContext,
|
||||
transformationType);
|
||||
if (transformationService != null) {
|
||||
transformedResponse = transformationService.transform(transformationFunction, response);
|
||||
} else {
|
||||
transformedResponse = response;
|
||||
logger.warn("Couldn't transform response because transformationService of type '{}' is unavailable",
|
||||
transformationType);
|
||||
}
|
||||
} catch (TransformationException te) {
|
||||
logger.warn("An exception occurred while transforming '{}' with '{}' : '{}'", response, transformation,
|
||||
te.getMessage());
|
||||
|
||||
// in case of an error we return the response without any transformation
|
||||
transformedResponse = response;
|
||||
}
|
||||
|
||||
logger.debug("Transformed response is '{}'", transformedResponse);
|
||||
return transformedResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a transformation configuration string into its two parts - the
|
||||
* transformation type and the function/pattern to apply.
|
||||
*
|
||||
* @param transformation the string to split
|
||||
* @return a string array with exactly two entries for the type and the function
|
||||
*/
|
||||
protected String[] splitTransformationConfig(String transformation) {
|
||||
Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("given transformation function '" + transformation
|
||||
+ "' does not follow the expected pattern '<function>(<pattern>)'");
|
||||
}
|
||||
matcher.reset();
|
||||
|
||||
matcher.find();
|
||||
String type = matcher.group(1);
|
||||
String pattern = matcher.group(2);
|
||||
|
||||
return new String[] { type, pattern };
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the command string into an array.
|
||||
* Either invokes the shell and passes using the "c" option
|
||||
* or (if command already starts with one of the shells) splits by space.
|
||||
*
|
||||
* @param shell (path), picks to first one to execute the command
|
||||
* @param cOption "c"-option string
|
||||
* @param commandLine to execute
|
||||
* @return command array
|
||||
*/
|
||||
protected String[] createCmdArray(String[] shell, String cOption, String commandLine) {
|
||||
boolean startsWithShell = false;
|
||||
for (String sh : shell) {
|
||||
if (commandLine.startsWith(sh + " ")) {
|
||||
startsWithShell = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!startsWithShell) {
|
||||
return new String[] { shell[0], cOption, commandLine };
|
||||
} else {
|
||||
logger.debug("Splitting by spaces");
|
||||
try {
|
||||
return commandLine.split(" ");
|
||||
} catch (PatternSyntaxException e) {
|
||||
logger.warn("An exception occurred while splitting '{}' : '{}'", commandLine, e.getMessage());
|
||||
updateState(RUN, OnOffType.OFF);
|
||||
updateState(OUTPUT, new StringType(e.getMessage()));
|
||||
return new String[] {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about which operating system openHAB is running on.
|
||||
* Found on https://stackoverflow.com/a/31547504/7508309, slightly modified
|
||||
*
|
||||
* @author Constantin Piber (for Memin) - Initial contribution
|
||||
*/
|
||||
public enum OS {
|
||||
WINDOWS,
|
||||
LINUX,
|
||||
MAC,
|
||||
SOLARIS,
|
||||
UNKNOWN,
|
||||
NOT_SET
|
||||
}
|
||||
|
||||
private static OS os = OS.NOT_SET;
|
||||
|
||||
public static OS getOperatingSystemType() {
|
||||
if (os == OS.NOT_SET) {
|
||||
String operSys = System.getProperty("os.name").toLowerCase();
|
||||
if (operSys.contains("win")) {
|
||||
os = OS.WINDOWS;
|
||||
} else if (operSys.contains("nix") || operSys.contains("nux") || operSys.contains("aix")) {
|
||||
os = OS.LINUX;
|
||||
} else if (operSys.contains("mac")) {
|
||||
os = OS.MAC;
|
||||
} else if (operSys.contains("sunos")) {
|
||||
os = OS.SOLARIS;
|
||||
} else {
|
||||
os = OS.UNKNOWN;
|
||||
}
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
public static String getOperatingSystemName() {
|
||||
return System.getProperty("os.name");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="exec" 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>Exec Binding</name>
|
||||
<description>This is the binding to execute arbitrary shell commands</description>
|
||||
<author>Karel Goderis</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,33 @@
|
||||
###############
|
||||
# binding
|
||||
binding.exec.name = Exec Binding
|
||||
binding.exec.description = Binding zur Ausführung von Befehlen und zur Verarbeitung des Rückgabewerts
|
||||
|
||||
###############
|
||||
# thing types
|
||||
thing-type.exec.command.label = Befehl
|
||||
thing-type.exec.command.description = Befehl zur Ausführung und Verarbeitung des Rückgabewertes
|
||||
|
||||
# thing type configuration
|
||||
thing-type.config.exec.command.command.label = Befehl
|
||||
thing-type.config.exec.command.command.description = Der auszuführende Befehl
|
||||
thing-type.config.exec.command.transform.label = Transformation
|
||||
thing-type.config.exec.command.transform.description = Transformation des Rückgabewertes des Befehls, z.B. REGEX((.*)) liefert den Rückgabewert unverändert
|
||||
thing-type.config.exec.command.interval.label = Intervall
|
||||
thing-type.config.exec.command.interval.description = Intervall in Sekunden, in welchem der Befehl ausgeführt wird
|
||||
thing-type.config.exec.command.timeout.label = Timeout
|
||||
thing-type.config.exec.command.timeout.description = Timeout in Sekunden, nach dem die Ausführung des Befehls abgebrochen wird
|
||||
thing-type.config.exec.command.autorun.label = Autorun
|
||||
thing-type.config.exec.command.autorun.description = Wenn aktiv, dann wird der Befehl jedes Mal ausgeführt, wenn sich der Eingabewert ändert
|
||||
|
||||
# channel type
|
||||
channel-type.exec.output.label = Rückgabewert
|
||||
channel-type.exec.output.description = Rückgabewert der Befehlsausführung
|
||||
channel-type.exec.input.label = Eingabewert
|
||||
channel-type.exec.input.description = Eingabewert, der als zweiter Parameter an den Befehl übergeben wird
|
||||
channel-type.exec.exit.label = Rückgabestatus
|
||||
channel-type.exec.exit.description = Dokumentiert die erfolgreiche Ausführung
|
||||
channel-type.exec.run.label = Ausführung
|
||||
channel-type.exec.run.description = Steht während der Befehlsausführung auf ON; durch Setzen auf ON wird der Befehl sofort ausgeführt
|
||||
channel-type.exec.lastexecution.label = Zeitpunkt der letzten Ausführung
|
||||
channel-type.exec.lastexecution.description = Datum und Uhrzeit der letzten Ausführung des Befehls im Format yyyy-MM-dd'T'HH:mm:ss.SSSZ
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="exec"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="command">
|
||||
<label>Command</label>
|
||||
<description>The Command encapsulates a shell command to be executed</description>
|
||||
|
||||
<channels>
|
||||
<channel id="output" typeId="output"/>
|
||||
<channel id="input" typeId="input"/>
|
||||
<channel id="exit" typeId="exit"/>
|
||||
<channel id="run" typeId="run"/>
|
||||
<channel id="lastexecution" typeId="lastexecution"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="command" type="text" required="true">
|
||||
<label>Command</label>
|
||||
<description>The command to execute</description>
|
||||
</parameter>
|
||||
<parameter name="transform" type="text" required="false">
|
||||
<label>Transform</label>
|
||||
<description>The transformation to apply on the execution result, e.g. REGEX((.*))</description>
|
||||
<default>REGEX((.*))</default>
|
||||
</parameter>
|
||||
<parameter name="interval" type="integer" required="false">
|
||||
<label>Interval</label>
|
||||
<description>Interval, in seconds, the command will be repeatedly executed</description>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="timeout" type="integer" required="false">
|
||||
<label>Timeout</label>
|
||||
<description>Time out, in seconds, the execution of the command will time out</description>
|
||||
<default>15</default>
|
||||
</parameter>
|
||||
<parameter name="autorun" type="boolean" required="false">
|
||||
<label>Autorun</label>
|
||||
<description>When true, the command will execute each time the state of the input channel changes</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="output">
|
||||
<item-type>String</item-type>
|
||||
<label>Output</label>
|
||||
<description>Output of the last execution of the command</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="input">
|
||||
<item-type>String</item-type>
|
||||
<label>Input</label>
|
||||
<description>Input that will be passed as second parameter to the command</description>
|
||||
<state readOnly="false"></state>
|
||||
</channel-type>
|
||||
<channel-type id="exit">
|
||||
<item-type>Number</item-type>
|
||||
<label>Exit Value</label>
|
||||
<description>The exit value of the last execution of the command</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="run">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Running</label>
|
||||
<description>Send ON to execute the command and the current state tells whether it is running or not</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="lastexecution">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Execution</label>
|
||||
<description>Time/Date the command was last executed, in yyyy-MM-dd'T'HH:mm:ss.SSSZ format</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user