Introduce a profile for the generic SCRIPT transformation (#3292)
* Introduce a generic script profile Signed-off-by: Jan N. Klug <github@klug.nrw>pull/3469/head
parent
cb38d19360
commit
7bad9baa85
|
@ -25,6 +25,12 @@
|
|||
<artifactId>org.openhab.core.transform</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
*/
|
||||
package org.openhab.core.automation.module.script;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
@ -20,6 +24,7 @@ import java.util.concurrent.locks.Lock;
|
|||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.script.Compilable;
|
||||
import javax.script.CompiledScript;
|
||||
|
@ -29,8 +34,12 @@ import javax.script.ScriptException;
|
|||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
|
||||
import org.openhab.core.automation.module.script.profile.ScriptProfile;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.config.core.ConfigOptionProvider;
|
||||
import org.openhab.core.config.core.ParameterOption;
|
||||
import org.openhab.core.transform.Transformation;
|
||||
import org.openhab.core.transform.TransformationException;
|
||||
import org.openhab.core.transform.TransformationRegistry;
|
||||
|
@ -39,6 +48,8 @@ import org.osgi.service.component.annotations.Activate;
|
|||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -48,10 +59,13 @@ import org.slf4j.LoggerFactory;
|
|||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@Component(service = TransformationService.class, property = { "openhab.transform=SCRIPT" })
|
||||
@Component(service = { TransformationService.class, ScriptTransformationService.class,
|
||||
ConfigOptionProvider.class }, property = { "openhab.transform=SCRIPT" })
|
||||
@NonNullByDefault
|
||||
public class ScriptTransformationService implements TransformationService, RegistryChangeListener<Transformation> {
|
||||
public class ScriptTransformationService
|
||||
implements TransformationService, RegistryChangeListener<Transformation>, ConfigOptionProvider {
|
||||
public static final String OPENHAB_TRANSFORMATION_SCRIPT = "openhab-transformation-script-";
|
||||
private static final String PROFILE_CONFIG_URI = "profile:transform:SCRIPT";
|
||||
public static final String SUPPORTED_CONFIGURATION_TYPE = "script";
|
||||
|
||||
private static final Pattern SCRIPT_CONFIG_PATTERN = Pattern
|
||||
|
@ -65,6 +79,8 @@ public class ScriptTransformationService implements TransformationService, Regis
|
|||
private final Map<String, ScriptRecord> scriptCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final TransformationRegistry transformationRegistry;
|
||||
private final Map<String, String> supportedScriptTypes = new ConcurrentHashMap<>();
|
||||
|
||||
private final ScriptEngineManager scriptEngineManager;
|
||||
|
||||
@Activate
|
||||
|
@ -160,10 +176,10 @@ public class ScriptTransformationService implements TransformationService, Regis
|
|||
|
||||
// compile the script here _after_ setting context attributes, so that the script engine
|
||||
// can bind the attributes as variables during compilation. This primarily affects jruby.
|
||||
if (compiledScript == null && scriptEngineContainer.getScriptEngine() instanceof Compilable) {
|
||||
if (compiledScript == null
|
||||
&& scriptEngineContainer.getScriptEngine()instanceof Compilable scriptEngine) {
|
||||
// no compiled script available but compiling is supported
|
||||
compiledScript = ((Compilable) scriptEngineContainer.getScriptEngine())
|
||||
.compile(scriptRecord.script);
|
||||
compiledScript = scriptEngine.compile(scriptRecord.script);
|
||||
scriptRecord.compiledScript = compiledScript;
|
||||
}
|
||||
|
||||
|
@ -211,14 +227,13 @@ public class ScriptTransformationService implements TransformationService, Regis
|
|||
}
|
||||
|
||||
private void disposeScriptEngine(ScriptEngine scriptEngine) {
|
||||
if (scriptEngine instanceof AutoCloseable) {
|
||||
if (scriptEngine instanceof AutoCloseable closableScriptEngine) {
|
||||
// we cannot not use ScheduledExecutorService.execute here as it might execute the task in the calling
|
||||
// thread (calling ScriptEngine.close in the same thread may result in a deadlock if the ScriptEngine
|
||||
// tries to Thread.join)
|
||||
scheduler.schedule(() -> {
|
||||
AutoCloseable closeable = (AutoCloseable) scriptEngine;
|
||||
try {
|
||||
closeable.close();
|
||||
closableScriptEngine.close();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error while closing script engine", e);
|
||||
}
|
||||
|
@ -228,6 +243,38 @@ public class ScriptTransformationService implements TransformationService, Regis
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
|
||||
@Nullable Locale locale) {
|
||||
if (PROFILE_CONFIG_URI.equals(uri.toString())) {
|
||||
if (ScriptProfile.CONFIG_TO_HANDLER_SCRIPT.equals(param)
|
||||
|| ScriptProfile.CONFIG_TO_ITEM_SCRIPT.equals(param)) {
|
||||
return transformationRegistry.getTransformations(List.of(SUPPORTED_CONFIGURATION_TYPE)).stream()
|
||||
.map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
|
||||
}
|
||||
if (ScriptProfile.CONFIG_SCRIPT_LANGUAGE.equals(param)) {
|
||||
return supportedScriptTypes.entrySet().stream().map(e -> new ParameterOption(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link ScriptEngineFactory}s are added/removed, this method will cache all available script types
|
||||
*/
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
|
||||
Map.Entry<String, String> parameterOption = ScriptEngineFactoryHelper.getParameterOption(engineFactory);
|
||||
if (parameterOption != null) {
|
||||
supportedScriptTypes.put(parameterOption.getKey(), parameterOption.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void unsetScriptEngineFactory(ScriptEngineFactory engineFactory) {
|
||||
supportedScriptTypes.remove(ScriptEngineFactoryHelper.getPreferredMimeType(engineFactory));
|
||||
}
|
||||
|
||||
private static class ScriptRecord {
|
||||
public String script = "";
|
||||
public @Nullable ScriptEngineContainer scriptEngineContainer;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.core.automation.module.script.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.module.script.ScriptEngineFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ScriptEngineFactoryHelper} contains helper methods for handling script engines
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ScriptEngineFactoryHelper {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ScriptEngineFactoryHelper.class);
|
||||
|
||||
private ScriptEngineFactoryHelper() {
|
||||
// prevent instantiation of static utility class
|
||||
}
|
||||
|
||||
public static Map.@Nullable Entry<String, String> getParameterOption(ScriptEngineFactory engineFactory) {
|
||||
List<String> scriptTypes = engineFactory.getScriptTypes();
|
||||
if (!scriptTypes.isEmpty()) {
|
||||
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptTypes.get(0));
|
||||
if (scriptEngine != null) {
|
||||
Map.Entry<String, String> parameterOption = Map.entry(getPreferredMimeType(engineFactory),
|
||||
getLanguageName(scriptEngine.getFactory()));
|
||||
LOGGER.trace("ParameterOptions: {}", parameterOption);
|
||||
return parameterOption;
|
||||
} else {
|
||||
LOGGER.trace("setScriptEngineFactory: engine was null");
|
||||
}
|
||||
} else {
|
||||
LOGGER.trace("addScriptEngineFactory: scriptTypes was empty");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getPreferredMimeType(ScriptEngineFactory factory) {
|
||||
List<String> mimeTypes = new ArrayList<>(factory.getScriptTypes());
|
||||
mimeTypes.removeIf(mimeType -> !mimeType.contains("application") || "application/python".equals(mimeType));
|
||||
return mimeTypes.isEmpty() ? factory.getScriptTypes().get(0) : mimeTypes.get(0);
|
||||
}
|
||||
|
||||
public static String getLanguageName(javax.script.ScriptEngineFactory factory) {
|
||||
return String.format("%s (%s)",
|
||||
factory.getLanguageName().substring(0, 1).toUpperCase() + factory.getLanguageName().substring(1),
|
||||
factory.getLanguageVersion());
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.Visibility;
|
||||
import org.openhab.core.automation.module.script.ScriptEngineFactory;
|
||||
import org.openhab.core.automation.module.script.internal.ScriptEngineFactoryHelper;
|
||||
import org.openhab.core.automation.module.script.internal.handler.AbstractScriptModuleHandler;
|
||||
import org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler;
|
||||
import org.openhab.core.automation.module.script.internal.handler.ScriptConditionHandler;
|
||||
|
@ -146,21 +147,14 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
|
|||
*/
|
||||
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
|
||||
public void setScriptEngineFactory(ScriptEngineFactory engineFactory) {
|
||||
List<String> scriptTypes = engineFactory.getScriptTypes();
|
||||
if (!scriptTypes.isEmpty()) {
|
||||
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptTypes.get(0));
|
||||
if (scriptEngine != null) {
|
||||
boolean notifyListeners = parameterOptions.isEmpty();
|
||||
parameterOptions.put(getPreferredMimeType(engineFactory), getLanguageName(scriptEngine.getFactory()));
|
||||
logger.trace("ParameterOptions: {}", parameterOptions);
|
||||
if (notifyListeners) {
|
||||
notifyModuleTypesAdded();
|
||||
}
|
||||
} else {
|
||||
logger.trace("setScriptEngineFactory: engine was null");
|
||||
Map.Entry<String, String> parameterOption = ScriptEngineFactoryHelper.getParameterOption(engineFactory);
|
||||
if (parameterOption != null) {
|
||||
boolean notifyListeners = parameterOptions.isEmpty();
|
||||
parameterOptions.put(parameterOption.getKey(), parameterOption.getValue());
|
||||
logger.trace("ParameterOptions: {}", parameterOptions);
|
||||
if (notifyListeners) {
|
||||
notifyModuleTypesAdded();
|
||||
}
|
||||
} else {
|
||||
logger.trace("addScriptEngineFactory: scriptTypes was empty");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,7 +163,7 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
|
|||
if (!scriptTypes.isEmpty()) {
|
||||
ScriptEngine scriptEngine = engineFactory.createScriptEngine(scriptTypes.get(0));
|
||||
if (scriptEngine != null) {
|
||||
parameterOptions.remove(getPreferredMimeType(engineFactory));
|
||||
parameterOptions.remove(ScriptEngineFactoryHelper.getPreferredMimeType(engineFactory));
|
||||
logger.trace("ParameterOptions: {}", parameterOptions);
|
||||
if (parameterOptions.isEmpty()) {
|
||||
notifyModuleTypesRemoved();
|
||||
|
@ -181,16 +175,4 @@ public class ScriptModuleTypeProvider extends AbstractProvider<ModuleType> imple
|
|||
logger.trace("unsetScriptEngineFactory: scriptTypes was empty");
|
||||
}
|
||||
}
|
||||
|
||||
private String getPreferredMimeType(ScriptEngineFactory factory) {
|
||||
List<String> mimeTypes = new ArrayList<>(factory.getScriptTypes());
|
||||
mimeTypes.removeIf(mimeType -> !mimeType.contains("application") || "application/python".equals(mimeType));
|
||||
return mimeTypes.isEmpty() ? factory.getScriptTypes().get(0) : mimeTypes.get(0);
|
||||
}
|
||||
|
||||
private String getLanguageName(javax.script.ScriptEngineFactory factory) {
|
||||
return String.format("%s (%s)",
|
||||
factory.getLanguageName().substring(0, 1).toUpperCase() + factory.getLanguageName().substring(1),
|
||||
factory.getLanguageVersion());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.core.automation.module.script.profile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.script.ScriptException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.ConfigParser;
|
||||
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||
import org.openhab.core.thing.profiles.ProfileContext;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||
import org.openhab.core.thing.profiles.StateProfile;
|
||||
import org.openhab.core.transform.TransformationException;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.Type;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ScriptProfile} is generic profile for managing values with scripts
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ScriptProfile implements StateProfile {
|
||||
|
||||
public static final String CONFIG_SCRIPT_LANGUAGE = "scriptLanguage";
|
||||
public static final String CONFIG_TO_ITEM_SCRIPT = "toItemScript";
|
||||
public static final String CONFIG_TO_HANDLER_SCRIPT = "toHandlerScript";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ScriptProfile.class);
|
||||
|
||||
private final ProfileCallback callback;
|
||||
private final TransformationService transformationService;
|
||||
|
||||
private final List<Class<? extends State>> acceptedDataTypes;
|
||||
private final List<Class<? extends Command>> acceptedCommandTypes;
|
||||
private final List<Class<? extends Command>> handlerAcceptedCommandTypes;
|
||||
|
||||
private final String scriptLanguage;
|
||||
private final String toItemScript;
|
||||
private final String toHandlerScript;
|
||||
|
||||
private final boolean isConfigured;
|
||||
|
||||
public ScriptProfile(ProfileCallback callback, ProfileContext profileContext,
|
||||
TransformationService transformationService) {
|
||||
this.callback = callback;
|
||||
this.transformationService = transformationService;
|
||||
|
||||
this.acceptedCommandTypes = profileContext.getAcceptedCommandTypes();
|
||||
this.acceptedDataTypes = profileContext.getAcceptedDataTypes();
|
||||
this.handlerAcceptedCommandTypes = profileContext.getHandlerAcceptedCommandTypes();
|
||||
|
||||
this.scriptLanguage = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_SCRIPT_LANGUAGE),
|
||||
String.class, "");
|
||||
this.toItemScript = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_ITEM_SCRIPT),
|
||||
String.class, "");
|
||||
this.toHandlerScript = ConfigParser
|
||||
.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_HANDLER_SCRIPT), String.class, "");
|
||||
|
||||
if (scriptLanguage.isBlank()) {
|
||||
logger.error("Script language is not defined. Profile will discard all states and commands.");
|
||||
isConfigured = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (toItemScript.isBlank() && toHandlerScript.isBlank()) {
|
||||
logger.error(
|
||||
"Neither 'toItem' nor 'toHandler' script defined. Profile will discard all states and commands.");
|
||||
isConfigured = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isConfigured = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfileTypeUID getProfileTypeUID() {
|
||||
return ScriptProfileFactory.SCRIPT_PROFILE_UID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateUpdateFromItem(State state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommandFromItem(Command command) {
|
||||
if (isConfigured) {
|
||||
String returnValue = executeScript(toHandlerScript, command);
|
||||
if (returnValue != null) {
|
||||
// try to parse the value
|
||||
Command newCommand = TypeParser.parseCommand(handlerAcceptedCommandTypes, returnValue);
|
||||
if (newCommand != null) {
|
||||
callback.handleCommand(newCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommandFromHandler(Command command) {
|
||||
if (isConfigured) {
|
||||
String returnValue = executeScript(toItemScript, command);
|
||||
if (returnValue != null) {
|
||||
Command newCommand = TypeParser.parseCommand(acceptedCommandTypes, returnValue);
|
||||
if (newCommand != null) {
|
||||
callback.sendCommand(newCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateUpdateFromHandler(State state) {
|
||||
if (isConfigured) {
|
||||
String returnValue = executeScript(toItemScript, state);
|
||||
// special handling for UnDefType, it's not available in the TypeParser
|
||||
if ("UNDEF".equals(returnValue)) {
|
||||
callback.sendUpdate(UnDefType.UNDEF);
|
||||
} else if ("NULL".equals(returnValue)) {
|
||||
callback.sendUpdate(UnDefType.NULL);
|
||||
} else if (returnValue != null) {
|
||||
State newState = TypeParser.parseState(acceptedDataTypes, returnValue);
|
||||
if (newState != null) {
|
||||
callback.sendUpdate(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String executeScript(String script, Type input) {
|
||||
if (!script.isBlank()) {
|
||||
try {
|
||||
return transformationService.transform(scriptLanguage + ":" + script, input.toFullString());
|
||||
} catch (TransformationException e) {
|
||||
if (e.getCause() instanceof ScriptException) {
|
||||
logger.error("Failed to process script '{}': {}", script, e.getCause().getMessage());
|
||||
} else {
|
||||
logger.error("Failed to process script '{}': {}", script, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.core.automation.module.script.profile;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.module.script.ScriptTransformationService;
|
||||
import org.openhab.core.thing.profiles.Profile;
|
||||
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||
import org.openhab.core.thing.profiles.ProfileContext;
|
||||
import org.openhab.core.thing.profiles.ProfileFactory;
|
||||
import org.openhab.core.thing.profiles.ProfileType;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeBuilder;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeProvider;
|
||||
import org.openhab.core.thing.profiles.ProfileTypeUID;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link ScriptProfileFactory} creates {@link ScriptProfile} instances
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@Component(service = { ScriptProfileFactory.class, ProfileFactory.class, ProfileTypeProvider.class })
|
||||
@NonNullByDefault
|
||||
public class ScriptProfileFactory implements ProfileFactory, ProfileTypeProvider {
|
||||
|
||||
public static final ProfileTypeUID SCRIPT_PROFILE_UID = new ProfileTypeUID(
|
||||
TransformationService.TRANSFORM_PROFILE_SCOPE, "SCRIPT");
|
||||
|
||||
private static final ProfileType PROFILE_TYPE_SCRIPT = ProfileTypeBuilder.newState(SCRIPT_PROFILE_UID, "Script")
|
||||
.build();
|
||||
|
||||
private final ScriptTransformationService transformationService;
|
||||
|
||||
@Activate
|
||||
public ScriptProfileFactory(final @Reference ScriptTransformationService transformationService) {
|
||||
this.transformationService = transformationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
|
||||
ProfileContext profileContext) {
|
||||
if (SCRIPT_PROFILE_UID.equals(profileTypeUID)) {
|
||||
return new ScriptProfile(callback, profileContext, transformationService);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
|
||||
return Set.of(SCRIPT_PROFILE_UID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
|
||||
return Set.of(PROFILE_TYPE_SCRIPT);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?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="profile:transform:SCRIPT">
|
||||
<parameter name="scriptLanguage" type="text" required="true">
|
||||
<label>Script Language</label>
|
||||
<description>MIME-type ("application/vnd.openhab.dsl.rule") of the scripting language</description>
|
||||
</parameter>
|
||||
<parameter name="toItemScript" type="text">
|
||||
<label>To Item Script</label>
|
||||
<description>The Script for transforming states and commands from handler to item.</description>
|
||||
</parameter>
|
||||
<parameter name="toHandlerScript" type="text">
|
||||
<label>To Handler Script</label>
|
||||
<description>The Script for transforming states and commands from item to handler.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
|
@ -0,0 +1,6 @@
|
|||
profile.system.script.scriptLanguage.label = Script Language
|
||||
profile.system.script.scriptLanguage.description = MIME-type ("application/vnd.openhab.dsl.rule") of the scripting language
|
||||
profile.system.script.toItemScript.label = To Item Script
|
||||
profile.system.script.toItemScript.description = The Script for transforming states and commands from handler to item.
|
||||
profile.system.script.toHandlerScript.label = To Handler Script
|
||||
profile.system.script.toHandlerScript.description = The Script for transforming states and commands from item to handler.
|
|
@ -0,0 +1,307 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2023 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.core.automation.module.script.profile;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_SCRIPT_LANGUAGE;
|
||||
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_TO_HANDLER_SCRIPT;
|
||||
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_TO_ITEM_SCRIPT;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.test.java.JavaTest;
|
||||
import org.openhab.core.thing.profiles.ProfileCallback;
|
||||
import org.openhab.core.thing.profiles.ProfileContext;
|
||||
import org.openhab.core.transform.TransformationException;
|
||||
import org.openhab.core.transform.TransformationService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link ScriptProfileTest} contains tests for the {@link ScriptProfile}
|
||||
*
|
||||
* @author Jan N. Klug - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@NonNullByDefault
|
||||
public class ScriptProfileTest extends JavaTest {
|
||||
private @Mock @NonNullByDefault({}) ProfileCallback profileCallback;
|
||||
|
||||
private @Mock @NonNullByDefault({}) TransformationService transformationServiceMock;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws TransformationException {
|
||||
when(transformationServiceMock.transform(any(), any())).thenReturn("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptDefined() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL").build();
|
||||
|
||||
setupInterceptedLogger(ScriptProfile.class, LogLevel.ERROR);
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(OnOffType.ON);
|
||||
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
|
||||
scriptProfile.onCommandFromItem(OnOffType.ON);
|
||||
|
||||
verify(transformationServiceMock, never()).transform(any(), any());
|
||||
verify(profileCallback, never()).handleCommand(any());
|
||||
verify(profileCallback, never()).sendUpdate(any());
|
||||
verify(profileCallback, never()).sendCommand(any());
|
||||
|
||||
assertLogMessage(ScriptProfile.class, LogLevel.ERROR,
|
||||
"Neither 'toItem' nor 'toHandler' script defined. Profile will discard all states and commands.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScriptNotExecutedAndNoValueForwardedToCallbackIfNoScriptLanguageDefined()
|
||||
throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
|
||||
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
|
||||
.withAcceptedDataTypes(List.of(PercentType.class))
|
||||
.withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
|
||||
|
||||
setupInterceptedLogger(ScriptProfile.class, LogLevel.ERROR);
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(OnOffType.ON);
|
||||
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
|
||||
scriptProfile.onCommandFromItem(OnOffType.ON);
|
||||
|
||||
verify(transformationServiceMock, never()).transform(any(), any());
|
||||
verify(profileCallback, never()).handleCommand(any());
|
||||
verify(profileCallback, never()).sendUpdate(any());
|
||||
verify(profileCallback, never()).sendCommand(any());
|
||||
|
||||
assertLogMessage(ScriptProfile.class, LogLevel.ERROR,
|
||||
"Script language is not defined. Profile will discard all states and commands.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scriptExecutionErrorForwardsNoValueToCallback() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
|
||||
.withToItemScript("inScript").withToHandlerScript("outScript").build();
|
||||
|
||||
when(transformationServiceMock.transform(any(), any()))
|
||||
.thenThrow(new TransformationException("intentional failure"));
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(OnOffType.ON);
|
||||
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
|
||||
scriptProfile.onCommandFromItem(OnOffType.ON);
|
||||
|
||||
verify(transformationServiceMock, times(3)).transform(any(), any());
|
||||
verify(profileCallback, never()).handleCommand(any());
|
||||
verify(profileCallback, never()).sendUpdate(any());
|
||||
verify(profileCallback, never()).sendCommand(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scriptExecutionResultNullForwardsNoValueToCallback() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
|
||||
.withToItemScript("inScript").withToHandlerScript("outScript").build();
|
||||
|
||||
when(transformationServiceMock.transform(any(), any())).thenReturn(null);
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(OnOffType.ON);
|
||||
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
|
||||
scriptProfile.onCommandFromItem(OnOffType.ON);
|
||||
|
||||
verify(transformationServiceMock, times(3)).transform(any(), any());
|
||||
verify(profileCallback, never()).handleCommand(any());
|
||||
verify(profileCallback, never()).sendUpdate(any());
|
||||
verify(profileCallback, never()).sendCommand(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scriptExecutionResultForwardsTransformedValueToCallback() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
|
||||
.withToItemScript("inScript").withToHandlerScript("outScript")
|
||||
.withAcceptedCommandTypes(List.of(OnOffType.class)).withAcceptedDataTypes(List.of(OnOffType.class))
|
||||
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
|
||||
|
||||
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onCommandFromItem(DecimalType.ZERO);
|
||||
|
||||
verify(transformationServiceMock, times(3)).transform(any(), any());
|
||||
verify(profileCallback).handleCommand(OnOffType.OFF);
|
||||
verify(profileCallback).sendUpdate(OnOffType.OFF);
|
||||
verify(profileCallback).sendCommand(OnOffType.OFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyToItemScriptDoesNotForwardOutboundCommands() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
|
||||
.withToItemScript("inScript").withAcceptedCommandTypes(List.of(OnOffType.class))
|
||||
.withAcceptedDataTypes(List.of(OnOffType.class))
|
||||
.withHandlerAcceptedCommandTypes(List.of(DecimalType.class)).build();
|
||||
|
||||
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onCommandFromItem(DecimalType.ZERO);
|
||||
|
||||
verify(transformationServiceMock, times(2)).transform(any(), any());
|
||||
verify(profileCallback, never()).handleCommand(any());
|
||||
verify(profileCallback).sendUpdate(OnOffType.OFF);
|
||||
verify(profileCallback).sendCommand(OnOffType.OFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyToHandlerScriptDoesNotForwardInboundCommands() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
|
||||
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(DecimalType.class))
|
||||
.withAcceptedDataTypes(List.of(DecimalType.class))
|
||||
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
|
||||
|
||||
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onCommandFromItem(DecimalType.ZERO);
|
||||
|
||||
verify(transformationServiceMock).transform(any(), any());
|
||||
verify(profileCallback).handleCommand(OnOffType.OFF);
|
||||
verify(profileCallback, never()).sendUpdate(any());
|
||||
verify(profileCallback, never()).sendCommand(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompatibleStateOrCommandNotForwardedToCallback() throws TransformationException {
|
||||
ProfileContext profileContext = ProfileContextBuilder.create().withScriptLanguage("customDSL")
|
||||
.withToItemScript("inScript").withToHandlerScript("outScript")
|
||||
.withAcceptedCommandTypes(List.of(DecimalType.class)).withAcceptedDataTypes(List.of(PercentType.class))
|
||||
.withHandlerAcceptedCommandTypes(List.of(HSBType.class)).build();
|
||||
|
||||
when(transformationServiceMock.transform(any(), any())).thenReturn(OnOffType.OFF.toString());
|
||||
|
||||
ScriptProfile scriptProfile = new ScriptProfile(profileCallback, profileContext, transformationServiceMock);
|
||||
|
||||
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
|
||||
scriptProfile.onCommandFromItem(DecimalType.ZERO);
|
||||
|
||||
verify(transformationServiceMock, times(3)).transform(any(), any());
|
||||
verify(profileCallback, never()).handleCommand(any());
|
||||
verify(profileCallback, never()).sendUpdate(any());
|
||||
verify(profileCallback, never()).sendCommand(any());
|
||||
}
|
||||
|
||||
private static class ProfileContextBuilder {
|
||||
private final Map<String, Object> configuration = new HashMap<>();
|
||||
private List<Class<? extends State>> acceptedDataTypes = List.of();
|
||||
private List<Class<? extends Command>> acceptedCommandTypes = List.of();
|
||||
private List<Class<? extends Command>> handlerAcceptedCommandTypes = List.of();
|
||||
|
||||
public static ProfileContextBuilder create() {
|
||||
return new ProfileContextBuilder();
|
||||
}
|
||||
|
||||
public ProfileContextBuilder withScriptLanguage(String scriptLanguage) {
|
||||
configuration.put(CONFIG_SCRIPT_LANGUAGE, scriptLanguage);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileContextBuilder withToItemScript(String toItem) {
|
||||
configuration.put(CONFIG_TO_ITEM_SCRIPT, toItem);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileContextBuilder withToHandlerScript(String toHandlerScript) {
|
||||
configuration.put(CONFIG_TO_HANDLER_SCRIPT, toHandlerScript);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileContextBuilder withAcceptedDataTypes(List<Class<? extends State>> acceptedDataTypes) {
|
||||
this.acceptedDataTypes = acceptedDataTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileContextBuilder withAcceptedCommandTypes(List<Class<? extends Command>> acceptedCommandTypes) {
|
||||
this.acceptedCommandTypes = acceptedCommandTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileContextBuilder withHandlerAcceptedCommandTypes(
|
||||
List<Class<? extends Command>> handlerAcceptedCommandTypes) {
|
||||
this.handlerAcceptedCommandTypes = handlerAcceptedCommandTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProfileContext build() {
|
||||
return new ProfileContext() {
|
||||
@Override
|
||||
public Configuration getConfiguration() {
|
||||
return new Configuration(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledExecutorService getExecutorService() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends State>> getAcceptedDataTypes() {
|
||||
return acceptedDataTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Command>> getAcceptedCommandTypes() {
|
||||
return acceptedCommandTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<? extends Command>> getHandlerAcceptedCommandTypes() {
|
||||
return handlerAcceptedCommandTypes;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue