Skip to content

Commit

Permalink
Enhance ThingActions UI support (#4392)
Browse files Browse the repository at this point in the history
* Enhance ThingActions UI support

Fixes #1745

Return config description parameters for the ActionInputs of ThingActions for the REST GET /action/{thingUID} and REST GET /module-types endpoints.
The config description parameters are only provided if all input parameters have a type that can be mapped to a config description parameter (String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, float, Float, double, Double, Number, DecimalType, QuantityType<?>, LocalDateTime, LocalDate, LocalTime, ZonedDateTime, Date, Instant and Duration).

Enhance the REST POST /actions/{thingUID}/{actionUid} endpoint (allows invoking Thing actions via REST) and the AnnotationActionHandler (allows invoking Thing actions from UI-rules) in order to be more flexible regarding the type of each provided argument value and to map the value to the expected data type. Number and string values will be accepted as inputs and the expected data type will be created from this value.

This will be used by the UI's Thing page and rule editor to allow invoking Thing actions through the UI or adding them to UI-bases rules.

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
  • Loading branch information
lolodomo authored Oct 20, 2024
1 parent 6d0a3b3 commit d431013
Show file tree
Hide file tree
Showing 10 changed files with 978 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@
import org.openhab.core.automation.type.Input;
import org.openhab.core.automation.type.ModuleTypeRegistry;
import org.openhab.core.automation.type.Output;
import org.openhab.core.automation.util.ActionInputsHelper;
import org.openhab.core.automation.util.ModuleBuilder;
import org.openhab.core.config.core.ConfigDescriptionParameter;
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 Down Expand Up @@ -77,6 +81,7 @@
* The {@link ThingActionsResource} allows retrieving and executing thing actions via REST API
*
* @author Jan N. Klug - Initial contribution
* @author Laurent Garnier - API enhanced to be able to run thing actions in Main UI
*/
@Component
@JaxrsResource
Expand All @@ -91,15 +96,17 @@ public class ThingActionsResource implements RESTResource {

private final LocaleService localeService;
private final ModuleTypeRegistry moduleTypeRegistry;
private final ActionInputsHelper actionInputsHelper;

Map<ThingUID, Map<String, ThingActions>> thingActionsMap = new ConcurrentHashMap<>();
private List<ModuleHandlerFactory> moduleHandlerFactories = new ArrayList<>();

@Activate
public ThingActionsResource(@Reference LocaleService localeService,
@Reference ModuleTypeRegistry moduleTypeRegistry) {
@Reference ModuleTypeRegistry moduleTypeRegistry, @Reference ActionInputsHelper actionInputsHelper) {
this.localeService = localeService;
this.moduleTypeRegistry = moduleTypeRegistry;
this.actionInputsHelper = actionInputsHelper;
}

@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
Expand Down Expand Up @@ -171,11 +178,27 @@ public Response getActions(@PathParam("thingUID") @Parameter(description = "thin
continue;
}

// Filter the configuration description parameters that correspond to inputs
List<ConfigDescriptionParameter> inputParameters = new ArrayList<>();
for (ConfigDescriptionParameter parameter : actionType.getConfigurationDescriptions()) {
if (actionType.getInputs().stream().anyMatch(i -> i.getName().equals(parameter.getName()))) {
inputParameters.add(parameter);
}
}
// If the resulting list of configuration description parameters is empty while the list of
// inputs is not empty, this is because the conversion of inputs into configuration description
// parameters failed for at least one input
if (inputParameters.isEmpty() && !actionType.getInputs().isEmpty()) {
inputParameters = null;
}

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 +244,9 @@ 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(actionInputsHelper.mapSerializedInputsToActionInputs(actionType, actionInputs)),
Map.of());
moduleHandlerFactory.ungetHandler(action, ruleUID, handler);
return Response.ok(returnValue).build();
} catch (Exception e) {
Expand All @@ -245,6 +270,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 @@ -29,13 +29,15 @@
import org.openhab.core.automation.type.ActionType;
import org.openhab.core.automation.type.Input;
import org.openhab.core.automation.type.Output;
import org.openhab.core.automation.util.ActionInputsHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* ActionHandler which is dynamically created upon annotation on services
*
* @author Stefan Triller - Initial contribution
* @author Laurent Garnier - Added ActionInputsHelper
*/
@NonNullByDefault
public class AnnotationActionHandler extends BaseActionModuleHandler {
Expand All @@ -47,13 +49,16 @@ public class AnnotationActionHandler extends BaseActionModuleHandler {
private final Method method;
private final ActionType moduleType;
private final Object actionProvider;
private final ActionInputsHelper actionInputsHelper;

public AnnotationActionHandler(Action module, ActionType mt, Method method, Object actionProvider) {
public AnnotationActionHandler(Action module, ActionType mt, Method method, Object actionProvider,
ActionInputsHelper actionInputsHelper) {
super(module);

this.method = method;
this.moduleType = mt;
this.actionProvider = actionProvider;
this.actionInputsHelper = actionInputsHelper;
}

@Override
Expand All @@ -69,7 +74,21 @@ public AnnotationActionHandler(Action module, ActionType mt, Method method, Obje
if (annotationsOnParam[0] instanceof ActionInput inputAnnotation) {
// check if the moduleType has a configdescription with this input
if (hasInput(moduleType, inputAnnotation.name())) {
args.add(i, context.get(inputAnnotation.name()));
Object value = context.get(inputAnnotation.name());
// fallback to configuration as this is where the UI stores the input values
if (value == null) {
Object configValue = module.getConfiguration().get(inputAnnotation.name());
if (configValue != null) {
try {
value = actionInputsHelper.mapSerializedInputToActionInput(
moduleType.getInputs().get(i), configValue);
} catch (IllegalArgumentException e) {
logger.debug("{} Input parameter is ignored.", e.getMessage());
// Ignore it and keep null in value
}
}
}
args.add(i, value);
} else {
logger.error(
"Annotated method defines input '{}' but the module type '{}' does not specify an input with this name.",
Expand All @@ -84,8 +103,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);
} else {
logger.debug(" - Argument {}: type {} value {}", i, 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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.openhab.core.automation.type.ActionType;
import org.openhab.core.automation.type.ModuleType;
import org.openhab.core.automation.type.ModuleTypeProvider;
import org.openhab.core.automation.util.ActionInputsHelper;
import org.openhab.core.common.registry.ProviderChangeListener;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
Expand All @@ -52,20 +53,25 @@
* from them
*
* @author Stefan Triller - Initial contribution
* @author Laurent Garnier - Injected components AnnotationActionModuleTypeHelper and ActionInputsHelper
*/
@NonNullByDefault
@Component(service = { ModuleTypeProvider.class, ModuleHandlerFactory.class })
public class AnnotatedActionModuleTypeProvider extends BaseModuleHandlerFactory implements ModuleTypeProvider {

private final Collection<ProviderChangeListener<ModuleType>> changeListeners = ConcurrentHashMap.newKeySet();
private final Map<String, Set<ModuleInformation>> moduleInformation = new ConcurrentHashMap<>();
private final AnnotationActionModuleTypeHelper helper = new AnnotationActionModuleTypeHelper();

private final AnnotationActionModuleTypeHelper helper;
private final ModuleTypeI18nService moduleTypeI18nService;
private final ActionInputsHelper actionInputsHelper;

@Activate
public AnnotatedActionModuleTypeProvider(final @Reference ModuleTypeI18nService moduleTypeI18nService) {
public AnnotatedActionModuleTypeProvider(final @Reference ModuleTypeI18nService moduleTypeI18nService,
final @Reference AnnotationActionModuleTypeHelper helper,
final @Reference ActionInputsHelper actionInputsHelper) {
this.moduleTypeI18nService = moduleTypeI18nService;
this.helper = helper;
this.actionInputsHelper = actionInputsHelper;
}

@Override
Expand Down Expand Up @@ -219,7 +225,7 @@ public Collection<String> getTypes() {
return null;
}
return new AnnotationActionHandler(actionModule, moduleType, finalMI.getMethod(),
finalMI.getActionProvider());
finalMI.getActionProvider(), actionInputsHelper);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,27 @@
import org.openhab.core.automation.type.Input;
import org.openhab.core.automation.type.ModuleTypeProvider;
import org.openhab.core.automation.type.Output;
import org.openhab.core.automation.util.ActionInputsHelper;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionParameter.Type;
import org.openhab.core.config.core.ConfigDescriptionParameterBuilder;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.ParameterOption;
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;

/**
* Helper methods for {@link AnnotatedActions} {@link ModuleTypeProvider}
*
* @author Stefan Triller - Initial contribution
* @author Florian Hotze - Added configuration description parameters for thing modules
* @author Laurent Garnier - Converted into a an OSGi component
*/
@NonNullByDefault
@Component(service = AnnotationActionModuleTypeHelper.class)
public class AnnotationActionModuleTypeHelper {

private final Logger logger = LoggerFactory.getLogger(AnnotationActionModuleTypeHelper.class);
Expand All @@ -61,6 +68,13 @@ public class AnnotationActionModuleTypeHelper {
private static final String SELECT_THING_LABEL = "Select Thing";
public static final String CONFIG_PARAM = "config";

private final ActionInputsHelper actionInputsHelper;

@Activate
public AnnotationActionModuleTypeHelper(final @Reference ActionInputsHelper actionInputsHelper) {
this.actionInputsHelper = actionInputsHelper;
}

public Collection<ModuleInformation> parseAnnotations(Object actionProvider) {
Class<?> clazz = actionProvider.getClass();
if (clazz.isAnnotationPresent(ActionScope.class)) {
Expand All @@ -77,7 +91,7 @@ public Collection<ModuleInformation> parseAnnotations(String name, Object action
for (Method method : methods) {
if (method.isAnnotationPresent(RuleAction.class)) {
List<Input> inputs = getInputsFromAction(method);
List<Output> outputs = getOutputsFromMethod(method);
List<Output> outputs = getOutputsFromAction(method);

RuleAction ruleAction = method.getAnnotation(RuleAction.class);
String uid = name + "." + method.getName();
Expand All @@ -86,10 +100,7 @@ public Collection<ModuleInformation> parseAnnotations(String name, Object action
ModuleInformation mi = new ModuleInformation(uid, actionProvider, method);
mi.setLabel(ruleAction.label());
mi.setDescription(ruleAction.description());
// We temporarily want to hide all ThingActions in UIs as we do not have a proper solution to enter
// their input values (see https://github.com/openhab/openhab-core/issues/1745)
// mi.setVisibility(ruleAction.visibility());
mi.setVisibility(Visibility.HIDDEN);
mi.setVisibility(ruleAction.visibility());
mi.setInputs(inputs);
mi.setOutputs(outputs);
mi.setTags(tags);
Expand Down Expand Up @@ -132,7 +143,7 @@ private List<Input> getInputsFromAction(Method method) {
return inputs;
}

private List<Output> getOutputsFromMethod(Method method) {
private List<Output> getOutputsFromAction(Method method) {
List<Output> outputs = new ArrayList<>();
if (method.isAnnotationPresent(ActionOutputs.class)) {
for (ActionOutput ruleActionOutput : method.getAnnotationsByType(ActionOutput.class)) {
Expand Down Expand Up @@ -170,8 +181,25 @@ private List<Output> getOutputsFromMethod(Method method) {
if (configParam != null) {
configDescriptions.add(configParam);
}
return new ActionType(uid, configDescriptions, mi.getLabel(), mi.getDescription(), mi.getTags(),
mi.getVisibility(), mi.getInputs(), mi.getOutputs());

Visibility visibility = mi.getVisibility();

if (kind == ActionModuleKind.THING) {
// we have a Thing module, so we have to map the inputs to config description parameters for the UI
try {
List<ConfigDescriptionParameter> inputConfigDescriptions = actionInputsHelper
.mapActionInputsToConfigDescriptionParameters(mi.getInputs());
configDescriptions.addAll(inputConfigDescriptions);
} catch (IllegalArgumentException e) {
// we have an input without a supported type, so hide the Thing action
visibility = Visibility.HIDDEN;
logger.debug("{} Thing action {} has an input with an unsupported type, hiding it in the UI.",
e.getMessage(), uid);
}
}

return new ActionType(uid, configDescriptions, mi.getLabel(), mi.getDescription(), mi.getTags(), visibility,
mi.getInputs(), mi.getOutputs());
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.openhab.core.automation.type.ActionType;
import org.openhab.core.automation.type.ModuleType;
import org.openhab.core.automation.type.ModuleTypeProvider;
import org.openhab.core.automation.util.ActionInputsHelper;
import org.openhab.core.common.registry.ProviderChangeListener;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
Expand All @@ -54,6 +55,7 @@
* ModuleTypeProvider that collects actions for {@link ThingHandler}s
*
* @author Stefan Triller - Initial contribution
* @author Laurent Garnier - Injected components AnnotationActionModuleTypeHelper and ActionInputsHelper
*/
@NonNullByDefault
@Component(service = { ModuleTypeProvider.class, ModuleHandlerFactory.class })
Expand All @@ -63,13 +65,17 @@ public class AnnotatedThingActionModuleTypeProvider extends BaseModuleHandlerFac

private final Collection<ProviderChangeListener<ModuleType>> changeListeners = ConcurrentHashMap.newKeySet();
private final Map<String, Set<ModuleInformation>> moduleInformation = new ConcurrentHashMap<>();
private final AnnotationActionModuleTypeHelper helper = new AnnotationActionModuleTypeHelper();

private final AnnotationActionModuleTypeHelper helper;
private final ModuleTypeI18nService moduleTypeI18nService;
private final ActionInputsHelper actionInputsHelper;

@Activate
public AnnotatedThingActionModuleTypeProvider(final @Reference ModuleTypeI18nService moduleTypeI18nService) {
public AnnotatedThingActionModuleTypeProvider(final @Reference ModuleTypeI18nService moduleTypeI18nService,
final @Reference AnnotationActionModuleTypeHelper helper,
final @Reference ActionInputsHelper actionInputsHelper) {
this.moduleTypeI18nService = moduleTypeI18nService;
this.helper = helper;
this.actionInputsHelper = actionInputsHelper;
}

@Override
Expand Down Expand Up @@ -236,7 +242,7 @@ public Collection<String> getTypes() {
return null;
}
return new AnnotationActionHandler(actionModule, moduleType, finalMI.getMethod(),
finalMI.getActionProvider());
finalMI.getActionProvider(), actionInputsHelper);
}
}
}
Expand Down
Loading

0 comments on commit d431013

Please sign in to comment.