Script profile: Separate toHandlerScript for commands and states (#4058)

* Script profile: Separate toHandlerScript for commands and states

This allows much more fine-grained control for the script profile.
E.g. it is now possible to mimic the behaviour of the `system:follow` profile, but apply a script transformation to the forwarded state.

Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
Florian Hotze
2024-02-06 21:29:19 +01:00
committed by GitHub
parent f4e83693fb
commit 6be00bd585
4 changed files with 154 additions and 52 deletions
@@ -43,6 +43,8 @@ public class ScriptProfile implements StateProfile {
public static final String CONFIG_TO_ITEM_SCRIPT = "toItemScript";
public static final String CONFIG_TO_HANDLER_SCRIPT = "toHandlerScript";
public static final String CONFIG_COMMAND_FROM_ITEM_SCRIPT = "commandFromItemScript";
public static final String CONFIG_STATE_FROM_ITEM_SCRIPT = "stateFromItemScript";
private final Logger logger = LoggerFactory.getLogger(ScriptProfile.class);
@@ -54,7 +56,8 @@ public class ScriptProfile implements StateProfile {
private final List<Class<? extends Command>> handlerAcceptedCommandTypes;
private final String toItemScript;
private final String toHandlerScript;
private final String commandFromItemScript;
private final String stateFromItemScript;
private final ProfileTypeUID profileTypeUID;
private final boolean isConfigured;
@@ -71,12 +74,22 @@ public class ScriptProfile implements StateProfile {
this.toItemScript = ConfigParser.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_ITEM_SCRIPT),
String.class, "");
this.toHandlerScript = ConfigParser
String toHandlerScript = ConfigParser
.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_TO_HANDLER_SCRIPT), String.class, "");
String localCommandFromItemScript = ConfigParser.valueAsOrElse(
profileContext.getConfiguration().get(CONFIG_COMMAND_FROM_ITEM_SCRIPT), String.class, "");
this.commandFromItemScript = localCommandFromItemScript.isBlank() ? toHandlerScript
: localCommandFromItemScript;
this.stateFromItemScript = ConfigParser
.valueAsOrElse(profileContext.getConfiguration().get(CONFIG_STATE_FROM_ITEM_SCRIPT), String.class, "");
if (toItemScript.isBlank() && toHandlerScript.isBlank()) {
if (!toHandlerScript.isBlank() && commandFromItemScript.isBlank()) {
logger.warn("'toHandlerScript' has been deprecated! Please use 'commandFromItemScript' instead.");
}
if (toItemScript.isBlank() && commandFromItemScript.isBlank() && stateFromItemScript.isBlank()) {
logger.error(
"Neither 'toItemScript' nor 'toHandlerScript' defined in link '{}'. Profile will discard all states and commands.",
"Neither 'toItemScript', 'commandFromItemScript' nor 'stateFromItemScript' defined in link '{}'. Profile will discard all states and commands.",
callback.getItemChannelLink());
isConfigured = false;
return;
@@ -92,18 +105,27 @@ public class ScriptProfile implements StateProfile {
@Override
public void onStateUpdateFromItem(State state) {
if (isConfigured) {
fromItem(stateFromItemScript, 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);
}
fromItem(commandFromItemScript, command);
}
}
private void fromItem(String script, Type type) {
String returnValue = executeScript(script, type);
if (returnValue != null) {
// try to parse the value
Command newCommand = TypeParser.parseCommand(handlerAcceptedCommandTypes, returnValue);
if (newCommand != null) {
callback.handleCommand(newCommand);
} else {
logger.debug("The given type {} could not be transformed to a command", type);
}
}
}
@@ -11,12 +11,26 @@
may return null to discard the updates/commands and not pass them through.</description>
<limitToOptions>false</limitToOptions>
</parameter>
<parameter name="toHandlerScript" type="text">
<label>Item To Thing Transformation</label>
<parameter name="commandFromItemScript" type="text">
<label>Item Command To Thing Transformation</label>
<description>The Script for transforming commands from the item to the Thing handler. The script may return null to
discard the commands and not pass them through.</description>
<limitToOptions>false</limitToOptions>
</parameter>
<parameter name="stateFromItemScript" type="text">
<label>Item State To Thing Transformation</label>
<description>The Script for transforming states from the item to the Thing handler. The script may return null to
discard the states and not pass them through.</description>
<limitToOptions>false</limitToOptions>
</parameter>
<parameter name="toHandlerScript" type="text">
<label>Item To Thing Transformation (DEPRECATED)</label>
<description>The Script for transforming commands from the item to the Thing handler. The script may return null to
discard the commands and not pass them through. This is deprecated and has been replaced by
'commandFromItemScript'.</description>
<limitToOptions>false</limitToOptions>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>
@@ -1,4 +1,8 @@
profile.system.script.toItemScript.label = Thing To Item Transformation
profile.system.script.toItemScript.description = The Script for transforming state updates and commands from the Thing handler to the item. The script may return null to discard the updates/commands and not pass them through.
profile.system.script.toHandlerScript.label = Item To Thing Transformation
profile.system.script.toHandlerScript.description = The Script for transforming commands from the item to the Thing handler. The script may return null to discard the commands and not pass them through.
profile.config.transform.SCRIPT.commandFromItemScript.label = Item Command To Thing Transformation
profile.config.transform.SCRIPT.commandFromItemScript.description = The Script for transforming commands from the item to the Thing handler. The script may return null to discard the commands and not pass them through.
profile.config.transform.SCRIPT.stateFromItemScript.label = Item State To Thing Transformation
profile.config.transform.SCRIPT.stateFromItemScript.description = The Script for transforming states from the item to the Thing handler. The script may return null to discard the states and not pass them through.
profile.config.transform.SCRIPT.toHandlerScript.label = Item To Thing Transformation (DEPRECATED)
profile.config.transform.SCRIPT.toHandlerScript.description = The Script for transforming commands from the item to the Thing handler. The script may return null to discard the commands and not pass them through. This is deprecated and has been replaced by 'commandFromItemScript'.
profile.config.transform.SCRIPT.toItemScript.label = Thing To Item Transformation
profile.config.transform.SCRIPT.toItemScript.description = The Script for transforming state updates and commands from the Thing handler to the item. The script may return null to discard the updates/commands and not pass them through.
@@ -18,6 +18,8 @@ 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_COMMAND_FROM_ITEM_SCRIPT;
import static org.openhab.core.automation.module.script.profile.ScriptProfile.CONFIG_STATE_FROM_ITEM_SCRIPT;
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;
@@ -90,14 +92,14 @@ public class ScriptProfileTest extends JavaTest {
verify(profileCallback, never()).sendCommand(any());
assertLogMessage(ScriptProfile.class, LogLevel.ERROR,
"Neither 'toItemScript' nor 'toHandlerScript' defined in link '" + link.toString()
+ "'. Profile will discard all states and commands.");
"Neither 'toItemScript', 'commandFromItemScript' nor 'stateFromItemScript' defined in link '"
+ link.toString() + "'. Profile will discard all states and commands.");
}
@Test
public void scriptExecutionErrorForwardsNoValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").build();
.withCommandFromItemScript("outScript").withStateFromItemScript("outScript").build();
when(transformationServiceMock.transform(any(), any()))
.thenThrow(new TransformationException("intentional failure"));
@@ -108,8 +110,9 @@ public class ScriptProfileTest extends JavaTest {
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
scriptProfile.onCommandFromItem(OnOffType.ON);
scriptProfile.onStateUpdateFromItem(OnOffType.ON);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(transformationServiceMock, times(4)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
@@ -118,7 +121,7 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void scriptExecutionResultNullForwardsNoValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").build();
.withCommandFromItemScript("outScript").withStateFromItemScript("outScript").build();
when(transformationServiceMock.transform(any(), any())).thenReturn(null);
@@ -128,8 +131,9 @@ public class ScriptProfileTest extends JavaTest {
scriptProfile.onCommandFromHandler(OnOffType.ON);
scriptProfile.onStateUpdateFromHandler(OnOffType.ON);
scriptProfile.onCommandFromItem(OnOffType.ON);
scriptProfile.onStateUpdateFromItem(OnOffType.ON);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(transformationServiceMock, times(4)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
@@ -138,8 +142,8 @@ public class ScriptProfileTest extends JavaTest {
@Test
public void scriptExecutionResultForwardsTransformedValueToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withToHandlerScript("outScript").withAcceptedCommandTypes(List.of(OnOffType.class))
.withAcceptedDataTypes(List.of(OnOffType.class))
.withCommandFromItemScript("outScript").withStateFromItemScript("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());
@@ -150,9 +154,10 @@ public class ScriptProfileTest extends JavaTest {
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
scriptProfile.onStateUpdateFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(3)).transform(any(), any());
verify(profileCallback).handleCommand(OnOffType.OFF);
verify(transformationServiceMock, times(4)).transform(any(), any());
verify(profileCallback, times(2)).handleCommand(OnOffType.OFF);
verify(profileCallback).sendUpdate(OnOffType.OFF);
verify(profileCallback).sendCommand(OnOffType.OFF);
}
@@ -171,6 +176,7 @@ public class ScriptProfileTest extends JavaTest {
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
scriptProfile.onStateUpdateFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(2)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
@@ -179,7 +185,74 @@ public class ScriptProfileTest extends JavaTest {
}
@Test
public void onlyToHandlerScriptDoesNotForwardInboundCommands() throws TransformationException {
public void onlyToHandlerCommandScriptDoesNotForwardInboundCommands() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withCommandFromItemScript("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(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
scriptProfile.onStateUpdateFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(1)).transform(any(), any());
verify(profileCallback, times(1)).handleCommand(OnOffType.OFF);
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@Test
public void onlyToHandlerStateScriptDoesNotForwardInboundCommands() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withStateFromItemScript("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(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
scriptProfile.onStateUpdateFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(1)).transform(any(), any());
verify(profileCallback, times(1)).handleCommand(OnOffType.OFF);
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@Test
public void incompatibleStateOrCommandNotForwardedToCallback() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withToItemScript("inScript")
.withCommandFromItemScript("outScript").withStateFromItemScript("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(mock(ProfileTypeUID.class), profileCallback, profileContext,
transformationServiceMock);
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
scriptProfile.onStateUpdateFromItem(DecimalType.ZERO);
verify(transformationServiceMock, times(4)).transform(any(), any());
verify(profileCallback, never()).handleCommand(any());
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@Test
public void fallbackToToHandlerScriptIfNotToHandlerCommandScript() throws TransformationException {
ProfileContext profileContext = ProfileContextBuilder.create().withToHandlerScript("outScript")
.withAcceptedCommandTypes(List.of(DecimalType.class)).withAcceptedDataTypes(List.of(DecimalType.class))
.withHandlerAcceptedCommandTypes(List.of(OnOffType.class)).build();
@@ -192,31 +265,10 @@ public class ScriptProfileTest extends JavaTest {
scriptProfile.onCommandFromHandler(DecimalType.ZERO);
scriptProfile.onStateUpdateFromHandler(DecimalType.ZERO);
scriptProfile.onCommandFromItem(DecimalType.ZERO);
scriptProfile.onStateUpdateFromItem(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().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(mock(ProfileTypeUID.class), 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(transformationServiceMock, times(1)).transform(any(), any());
verify(profileCallback, times(1)).handleCommand(OnOffType.OFF);
verify(profileCallback, never()).sendUpdate(any());
verify(profileCallback, never()).sendCommand(any());
}
@@ -241,6 +293,16 @@ public class ScriptProfileTest extends JavaTest {
return this;
}
public ProfileContextBuilder withCommandFromItemScript(String commandFromItemScript) {
configuration.put(CONFIG_COMMAND_FROM_ITEM_SCRIPT, commandFromItemScript);
return this;
}
public ProfileContextBuilder withStateFromItemScript(String stateFromItemScript) {
configuration.put(CONFIG_STATE_FROM_ITEM_SCRIPT, stateFromItemScript);
return this;
}
public ProfileContextBuilder withAcceptedDataTypes(List<Class<? extends State>> acceptedDataTypes) {
this.acceptedDataTypes = acceptedDataTypes;
return this;