Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance ThingActions UI support #4392

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,19 @@ 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) {
try {
value = actionInputsHelper.mapSerializedInputToActionInput(moduleType,
moduleType.getInputs().get(i),
module.getConfiguration().get(inputAnnotation.name()));
} 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 +101,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,28 @@
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.Deactivate;
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
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
* @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 +69,17 @@ 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;
}

@Deactivate
protected void deactivate() {
}
lolodomo marked this conversation as resolved.
Show resolved Hide resolved

public Collection<ModuleInformation> parseAnnotations(Object actionProvider) {
Class<?> clazz = actionProvider.getClass();
if (clazz.isAnnotationPresent(ActionScope.class)) {
Expand All @@ -77,7 +96,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 +105,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 +148,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 +186,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
lolodomo marked this conversation as resolved.
Show resolved Hide resolved
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