Skip to content

Commit

Permalink
Enhance actions REST API
Browse files Browse the repository at this point in the history
API GET /actions/{thingUID} now returns the input parameters also as a list of configuration description parameters.
It is provided only when all input parameters have a type than can be mapped to the type of a configuration description parameter.
It will be used in particular by Main UI to expose actions.

Also enhance the POST API (execute a thing action) in order to be more flexible regarding the type of each provided argument value and to map the value to the expected data type.

Related to #1745

Signed-off-by: Laurent Garnier <[email protected]>
  • Loading branch information
lolodomo committed Sep 29, 2024
1 parent a5c488d commit 2ca7f24
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
package org.openhab.core.automation.rest.internal;

import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -45,7 +50,11 @@
import org.openhab.core.automation.type.ModuleTypeRegistry;
import org.openhab.core.automation.type.Output;
import org.openhab.core.automation.util.ModuleBuilder;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionParameterBuilder;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.dto.ConfigDescriptionDTOMapper;
import org.openhab.core.config.core.dto.ConfigDescriptionParameterDTO;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
Expand All @@ -64,6 +73,8 @@
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -89,6 +100,8 @@
public class ThingActionsResource implements RESTResource {
public static final String PATH_THINGS = "actions";

private final Logger logger = LoggerFactory.getLogger(ThingActionsResource.class);

private final LocaleService localeService;
private final ModuleTypeRegistry moduleTypeRegistry;

Expand Down Expand Up @@ -171,11 +184,24 @@ public Response getActions(@PathParam("thingUID") @Parameter(description = "thin
continue;
}

List<ConfigDescriptionParameter> inputParameters = new ArrayList<>();
for (Input input : actionType.getInputs()) {
ConfigDescriptionParameter parameter = convertToConfigDescriptionParameter(input);
if (parameter != null) {
inputParameters.add(parameter);
} else {
inputParameters = null;
break;
}
}

ThingActionDTO actionDTO = new ThingActionDTO();
actionDTO.actionUid = actionType.getUID();
actionDTO.description = actionType.getDescription();
actionDTO.label = actionType.getLabel();
actionDTO.inputs = actionType.getInputs();
actionDTO.inputConfigDescriptions = inputParameters == null ? null
: ConfigDescriptionDTOMapper.mapParameters(inputParameters);
actionDTO.outputs = actionType.getOutputs();
actions.add(actionDTO);
}
Expand Down Expand Up @@ -221,7 +247,8 @@ public Response executeThingAction(@PathParam("thingUID") @Parameter(description
}

try {
Map<String, Object> returnValue = Objects.requireNonNullElse(handler.execute(actionInputs), Map.of());
Map<String, Object> returnValue = Objects.requireNonNullElse(
handler.execute(adjustTypForfMethodArguments(actionType, actionInputs)), Map.of());
moduleHandlerFactory.ungetHandler(action, ruleUID, handler);
return Response.ok(returnValue).build();
} catch (Exception e) {
Expand All @@ -230,6 +257,178 @@ public Response executeThingAction(@PathParam("thingUID") @Parameter(description
}
}

private @Nullable ConfigDescriptionParameter convertToConfigDescriptionParameter(Input input) {
boolean supported = true;
ConfigDescriptionParameter.Type parameterType = ConfigDescriptionParameter.Type.TEXT;
String defaultValue = null;
boolean required = false;
String context = null;
switch (input.getType()) {
case "boolean":
defaultValue = "false";
required = true;
case "java.lang.Boolean":
parameterType = ConfigDescriptionParameter.Type.BOOLEAN;
break;
case "byte":
case "short":
case "int":
case "long":
defaultValue = "0";
required = true;
case "java.lang.Byte":
case "java.lang.Short":
case "java.lang.Integer":
case "java.lang.Long":
parameterType = ConfigDescriptionParameter.Type.INTEGER;
break;
case "float":
case "double":
defaultValue = "0";
required = true;
case "java.lang.Float":
case "java.lang.Double":
parameterType = ConfigDescriptionParameter.Type.DECIMAL;
break;
case "java.lang.String":
parameterType = ConfigDescriptionParameter.Type.TEXT;
break;
case "java.time.LocalDate":
parameterType = ConfigDescriptionParameter.Type.TEXT;
context = "date";
break;
case "java.time.LocalTime":
parameterType = ConfigDescriptionParameter.Type.TEXT;
context = "time";
break;
case "java.time.LocalDateTime":
case "java.time.ZonedDateTime":
parameterType = ConfigDescriptionParameter.Type.TEXT;
context = "datetime";
break;
default:
supported = false;
break;
}
if (!supported) {
logger.warn("Unsupported input parameter '{}' having type {}", input.getName(), input.getType());
return null;
}

ConfigDescriptionParameterBuilder builder = ConfigDescriptionParameterBuilder
.create(input.getName(), parameterType).withLabel(input.getLabel())
.withDescription(input.getDescription()).withReadOnly(false)
.withRequired(required || input.isRequired()).withContext(context);
if (!input.getDefaultValue().isEmpty()) {
builder = builder.withDefault(input.getDefaultValue());
} else if (defaultValue != null) {
builder = builder.withDefault(defaultValue);
}
return builder.build();
}

private Map<String, Object> adjustTypForfMethodArguments(ActionType actionType, Map<String, Object> arguments) {
Map<String, Object> newArguments = new HashMap<>();
for (Input input : actionType.getInputs()) {
Object value = arguments.get(input.getName());
if (value == null) {
continue;
}
switch (input.getType()) {
case "byte":
case "java.lang.Byte":
if (value instanceof Double valueDouble) {
newArguments.put(input.getName(), Byte.valueOf(valueDouble.byteValue()));
} else if (value instanceof String valueString) {
newArguments.put(input.getName(), Byte.valueOf(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "short":
case "java.lang.Short":
if (value instanceof Double valueDouble) {
newArguments.put(input.getName(), Short.valueOf(valueDouble.shortValue()));
} else if (value instanceof String valueString) {
newArguments.put(input.getName(), Short.valueOf(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "int":
case "java.lang.Integer":
if (value instanceof Double valueDouble) {
newArguments.put(input.getName(), Integer.valueOf(valueDouble.intValue()));
} else if (value instanceof String valueString) {
newArguments.put(input.getName(), Integer.valueOf(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "long":
case "java.lang.Long":
if (value instanceof Double valueDouble) {
newArguments.put(input.getName(), Long.valueOf(valueDouble.longValue()));
} else if (value instanceof String valueString) {
newArguments.put(input.getName(), Long.valueOf(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "float":
case "java.lang.Float":
if (value instanceof Double valueDouble) {
newArguments.put(input.getName(), Float.valueOf(valueDouble.floatValue()));
} else if (value instanceof String valueString) {
newArguments.put(input.getName(), Float.valueOf(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "double":
case "java.lang.Double":
if (value instanceof String valueString) {
newArguments.put(input.getName(), Double.valueOf(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "java.time.LocalDate":
if (value instanceof String valueString) {
newArguments.put(input.getName(), LocalDate.parse(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "java.time.LocalTime":
if (value instanceof String valueString) {
newArguments.put(input.getName(), LocalTime.parse(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "java.time.LocalDateTime":
if (value instanceof String valueString) {
newArguments.put(input.getName(), LocalDateTime.parse(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
case "java.time.ZonedDateTime":
if (value instanceof String valueString) {
newArguments.put(input.getName(), ZonedDateTime.parse(valueString));
} else {
newArguments.put(input.getName(), value);
}
break;
default:
newArguments.put(input.getName(), value);
break;
}
}
return newArguments;
}

private @Nullable String getScope(ThingActions actions) {
ThingActionsScope scopeAnnotation = actions.getClass().getAnnotation(ThingActionsScope.class);
if (scopeAnnotation == null) {
Expand All @@ -245,6 +444,9 @@ private static class ThingActionDTO {
public @Nullable String description;

public List<Input> inputs = new ArrayList<>();

public @Nullable List<ConfigDescriptionParameterDTO> inputConfigDescriptions;

public List<Output> outputs = new ArrayList<>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,20 @@ public AnnotationActionHandler(Action module, ActionType mt, Method method, Obje
}

Object result = null;
Object @Nullable [] arguments = args.toArray();
if (arguments.length > 0 && logger.isDebugEnabled()) {
logger.debug("Calling action method {} with the following arguments:", method.getName());
for (int i = 0; i < arguments.length; i++) {
if (arguments[i] == null) {
logger.debug(" - Argument {}: null", i + 1);
} else {
logger.debug(" - Argument {}: type {} value {}", i + 1, arguments[i].getClass().getCanonicalName(),
arguments[i]);
}
}
}
try {
result = method.invoke(this.actionProvider, args.toArray());
result = method.invoke(this.actionProvider, arguments);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
logger.error("Could not call method '{}' from module type '{}'.", method, moduleType.getUID(), e);
}
Expand Down

0 comments on commit 2ca7f24

Please sign in to comment.