diff --git a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java index eca85f729..16f8c92a7 100644 --- a/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java +++ b/bundles/org.openhab.core.automation.rest/src/main/java/org/openhab/core/automation/rest/internal/ThingActionsResource.java @@ -44,8 +44,12 @@ import org.openhab.core.automation.type.ActionType; 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; @@ -77,6 +81,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; * 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 @@ -91,15 +96,17 @@ public class ThingActionsResource implements RESTResource { private final LocaleService localeService; private final ModuleTypeRegistry moduleTypeRegistry; + private final ActionInputsHelper actionInputsHelper; Map> thingActionsMap = new ConcurrentHashMap<>(); private List 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) @@ -171,11 +178,27 @@ public class ThingActionsResource implements RESTResource { continue; } + // Filter the configuration description parameters that correspond to inputs + List 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); } @@ -221,7 +244,9 @@ public class ThingActionsResource implements RESTResource { } try { - Map returnValue = Objects.requireNonNullElse(handler.execute(actionInputs), Map.of()); + Map returnValue = Objects.requireNonNullElse( + handler.execute(actionInputsHelper.mapSerializedInputsToActionInputs(actionType, actionInputs)), + Map.of()); moduleHandlerFactory.ungetHandler(action, ruleUID, handler); return Response.ok(returnValue).build(); } catch (Exception e) { @@ -245,6 +270,9 @@ public class ThingActionsResource implements RESTResource { public @Nullable String description; public List inputs = new ArrayList<>(); + + public @Nullable List inputConfigDescriptions; + public List outputs = new ArrayList<>(); } } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java index 9d1715dcf..53f3716f2 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/handler/AnnotationActionHandler.java @@ -29,6 +29,7 @@ import org.openhab.core.automation.handler.BaseActionModuleHandler; 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; @@ -36,6 +37,7 @@ 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 { @@ -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 @@ -69,7 +74,21 @@ public class AnnotationActionHandler extends BaseActionModuleHandler { 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.", @@ -84,8 +103,20 @@ public class AnnotationActionHandler extends BaseActionModuleHandler { } 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); } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java index 8e0d8a11b..9febb5d03 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/module/provider/AnnotatedActionModuleTypeProvider.java @@ -37,6 +37,7 @@ import org.openhab.core.automation.module.provider.i18n.ModuleTypeI18nService; 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; @@ -52,6 +53,7 @@ import org.osgi.service.component.annotations.ReferencePolicy; * from them * * @author Stefan Triller - Initial contribution + * @author Laurent Garnier - Injected components AnnotationActionModuleTypeHelper and ActionInputsHelper */ @NonNullByDefault @Component(service = { ModuleTypeProvider.class, ModuleHandlerFactory.class }) @@ -59,13 +61,17 @@ public class AnnotatedActionModuleTypeProvider extends BaseModuleHandlerFactory private final Collection> changeListeners = ConcurrentHashMap.newKeySet(); private final Map> 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 @@ -219,7 +225,7 @@ public class AnnotatedActionModuleTypeProvider extends BaseModuleHandlerFactory return null; } return new AnnotationActionHandler(actionModule, moduleType, finalMI.getMethod(), - finalMI.getActionProvider()); + finalMI.getActionProvider(), actionInputsHelper); } } } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java index 1daa68052..055bd1812 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/module/provider/AnnotationActionModuleTypeHelper.java @@ -39,11 +39,15 @@ import org.openhab.core.automation.type.ActionType; 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; @@ -51,8 +55,11 @@ 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); @@ -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 parseAnnotations(Object actionProvider) { Class clazz = actionProvider.getClass(); if (clazz.isAnnotationPresent(ActionScope.class)) { @@ -77,7 +91,7 @@ public class AnnotationActionModuleTypeHelper { for (Method method : methods) { if (method.isAnnotationPresent(RuleAction.class)) { List inputs = getInputsFromAction(method); - List outputs = getOutputsFromMethod(method); + List outputs = getOutputsFromAction(method); RuleAction ruleAction = method.getAnnotation(RuleAction.class); String uid = name + "." + method.getName(); @@ -86,10 +100,7 @@ public class AnnotationActionModuleTypeHelper { 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); @@ -132,7 +143,7 @@ public class AnnotationActionModuleTypeHelper { return inputs; } - private List getOutputsFromMethod(Method method) { + private List getOutputsFromAction(Method method) { List outputs = new ArrayList<>(); if (method.isAnnotationPresent(ActionOutputs.class)) { for (ActionOutput ruleActionOutput : method.getAnnotationsByType(ActionOutput.class)) { @@ -170,8 +181,25 @@ public class AnnotationActionModuleTypeHelper { 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 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; } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java index b942a6896..9adc32af9 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProvider.java @@ -35,6 +35,7 @@ import org.openhab.core.automation.module.provider.i18n.ModuleTypeI18nService; 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; @@ -54,6 +55,7 @@ import org.slf4j.LoggerFactory; * 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 }) @@ -63,13 +65,17 @@ public class AnnotatedThingActionModuleTypeProvider extends BaseModuleHandlerFac private final Collection> changeListeners = ConcurrentHashMap.newKeySet(); private final Map> 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 @@ -236,7 +242,7 @@ public class AnnotatedThingActionModuleTypeProvider extends BaseModuleHandlerFac return null; } return new AnnotationActionHandler(actionModule, moduleType, finalMI.getMethod(), - finalMI.getActionProvider()); + finalMI.getActionProvider(), actionInputsHelper); } } } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ActionInputsHelper.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ActionInputsHelper.java new file mode 100644 index 000000000..01c84ac66 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/util/ActionInputsHelper.java @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2010-2024 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.util; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.measure.Quantity; +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionParameterBuilder; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.types.util.UnitUtils; +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; + +/** + * This is an utility class to convert the {@link Input}s of a {@link ActionType} into a list of + * {@link ConfigDescriptionParameter}s and to convert serialised inputs to the Java types required by the + * {@link Input}s of a {@link ActionType}. + * + * @author Laurent Garnier & Florian Hotze - Initial contribution + */ +@NonNullByDefault +@Component(service = ActionInputsHelper.class) +public class ActionInputsHelper { + private static final Pattern QUANTITY_TYPE_PATTERN = Pattern + .compile("([a-z0-9]+\\.)*QuantityType<([a-z0-9]+\\.)*(?[A-Z][a-zA-Z0-9]*)>"); + + private final Logger logger = LoggerFactory.getLogger(ActionInputsHelper.class); + + private final UnitProvider unitProvider; + + @Activate + public ActionInputsHelper(final @Reference UnitProvider unitProvider) { + this.unitProvider = unitProvider; + } + + /** + * Maps a list of {@link Input}s to a list of {@link ConfigDescriptionParameter}s. + * + * @param inputs the list of inputs to map to config description parameters + * @return the list of config description parameters + * @throws IllegalArgumentException if at least one of the input parameters has an unsupported type + */ + public List mapActionInputsToConfigDescriptionParameters(List inputs) + throws IllegalArgumentException { + List configDescriptionParameters = new ArrayList<>(); + + for (Input input : inputs) { + configDescriptionParameters.add(mapActionInputToConfigDescriptionParameter(input)); + } + + return configDescriptionParameters; + } + + /** + * Maps an {@link Input} to a {@link ConfigDescriptionParameter}. + * + * @param input the input to map to a config description parameter + * @return the config description parameter + * @throws IllegalArgumentException if the input parameter has an unsupported type + */ + public ConfigDescriptionParameter mapActionInputToConfigDescriptionParameter(Input input) + throws IllegalArgumentException { + ConfigDescriptionParameter.Type parameterType = ConfigDescriptionParameter.Type.TEXT; + String defaultValue = null; + Unit unit = null; + boolean required = false; + String context = null; + Matcher matcher = QUANTITY_TYPE_PATTERN.matcher(input.getType()); + if (matcher.matches()) { + parameterType = ConfigDescriptionParameter.Type.DECIMAL; + try { + unit = getDefaultUnit(matcher.group("dimension")); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Input parameter '" + input.getName() + "' with type " + + input.getType() + "cannot be converted into a config description parameter!", e); + } + } else { + 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": + case "java.lang.Number": + case "org.openhab.core.library.types.DecimalType": + parameterType = ConfigDescriptionParameter.Type.DECIMAL; + break; + case "java.lang.String": + break; + case "java.time.LocalDate": + context = "date"; + break; + case "java.time.LocalTime": + context = "time"; + break; + case "java.time.LocalDateTime": + case "java.util.Date": + context = "datetime"; + break; + case "java.time.ZonedDateTime": + case "java.time.Instant": + case "java.time.Duration": + // There is no available configuration parameter context for these types. + // A text parameter is used. The expected value must respect a particular format specific + // to each of these types. + break; + default: + throw new IllegalArgumentException("Input parameter '" + input.getName() + "' with type " + + input.getType() + "cannot be converted into a config description parameter!"); + } + } + + ConfigDescriptionParameterBuilder builder = ConfigDescriptionParameterBuilder + .create(input.getName(), parameterType).withLabel(input.getLabel()) + .withDescription(input.getDescription()).withReadOnly(false) + .withRequired(required || input.isRequired()).withContext(context); + if (input.getDefaultValue() != null && !input.getDefaultValue().isEmpty()) { + builder = builder.withDefault(input.getDefaultValue()); + } else if (defaultValue != null) { + builder = builder.withDefault(defaultValue); + } + if (unit != null) { + builder = builder.withUnit(unit.getSymbol()); + } + return builder.build(); + } + + /** + * Maps serialised inputs to the Java types required by the {@link Input}s of the given {@link ActionType}. + * + * @param actionType the action type whose inputs to consider + * @param arguments the serialised arguments + * @return the mapped arguments + */ + public Map mapSerializedInputsToActionInputs(ActionType actionType, Map arguments) { + Map newArguments = new HashMap<>(); + for (Input input : actionType.getInputs()) { + Object value = arguments.get(input.getName()); + if (value != null) { + try { + newArguments.put(input.getName(), mapSerializedInputToActionInput(input, value)); + } catch (IllegalArgumentException e) { + logger.warn("{} Input parameter is ignored.", e.getMessage()); + } + } + } + return newArguments; + } + + /** + * Maps a serialised input to the Java type required by the given {@link Input}. + * + * @param input the input whose type to consider + * @param argument the serialised argument + * @return the mapped argument + * @throws IllegalArgumentException if the mapping failed + */ + public Object mapSerializedInputToActionInput(Input input, Object argument) throws IllegalArgumentException { + try { + Matcher matcher = QUANTITY_TYPE_PATTERN.matcher(input.getType()); + if (argument instanceof Double valueDouble) { + // When an integer value is provided as input value, the value type in the Map is Double. + // We have to convert Double type into the target type. + if (matcher.matches()) { + return new QuantityType<>(valueDouble, getDefaultUnit(matcher.group("dimension"))); + } else { + return switch (input.getType()) { + case "byte", "java.lang.Byte" -> Byte.valueOf(valueDouble.byteValue()); + case "short", "java.lang.Short" -> Short.valueOf(valueDouble.shortValue()); + case "int", "java.lang.Integer" -> Integer.valueOf(valueDouble.intValue()); + case "long", "java.lang.Long" -> Long.valueOf(valueDouble.longValue()); + case "float", "java.lang.Float" -> Float.valueOf(valueDouble.floatValue()); + case "org.openhab.core.library.types.DecimalType" -> new DecimalType(valueDouble); + default -> argument; + }; + } + } else if (argument instanceof String valueString) { + // String value is accepted to instantiate few target types + if (matcher.matches()) { + // The string can contain either a simple decimal value without unit or a decimal value with + // unit + try { + BigDecimal bigDecimal = new BigDecimal(valueString); + return new QuantityType<>(bigDecimal, getDefaultUnit(matcher.group("dimension"))); + } catch (NumberFormatException e) { + return new QuantityType<>(valueString); + } + } else { + return switch (input.getType()) { + case "boolean", "java.lang.Boolean" -> Boolean.valueOf(valueString.toLowerCase()); + case "byte", "java.lang.Byte" -> Byte.valueOf(valueString); + case "short", "java.lang.Short" -> Short.valueOf(valueString); + case "int", "java.lang.Integer" -> Integer.valueOf(valueString); + case "long", "java.lang.Long" -> Long.valueOf(valueString); + case "float", "java.lang.Float" -> Float.valueOf(valueString); + case "double", "java.lang.Double", "java.lang.Number" -> Double.valueOf(valueString); + case "org.openhab.core.library.types.DecimalType" -> new DecimalType(valueString); + case "java.time.LocalDate" -> + // Accepted format is: 2007-12-03 + LocalDate.parse(valueString); + case "java.time.LocalTime" -> + // Accepted format is: 10:15:30 + LocalTime.parse(valueString); + case "java.time.LocalDateTime" -> + // Accepted format is: 2007-12-03 10:15:30 + LocalDateTime.parse(valueString, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + case "java.util.Date" -> + // Accepted format is: 2007-12-03 10:15:30 + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(valueString); + case "java.time.ZonedDateTime" -> + // Accepted format is: 2007-12-03T10:15:30+01:00[Europe/Paris] + ZonedDateTime.parse(valueString); + case "java.time.Instant" -> + // Accepted format is: 2007-12-03T10:15:30.00Z + Instant.parse(valueString); + case "java.time.Duration" -> + // Accepted format is: P2DT17H25M30.5S + Duration.parse(valueString); + default -> argument; + }; + } + } + return argument; + } catch (IllegalArgumentException | DateTimeParseException | ParseException e) { + throw new IllegalArgumentException("Input parameter '" + input.getName() + "': converting value '" + + argument.toString() + "' into type " + input.getType() + " failed!"); + } + } + + private Unit getDefaultUnit(String dimensionName) throws IllegalArgumentException { + Class> dimension = UnitUtils.parseDimension(dimensionName); + if (dimension == null) { + throw new IllegalArgumentException("Unknown dimension " + dimensionName); + } + return unitProvider.getUnit((Class) dimension); + } +} diff --git a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/provider/AnnotationActionModuleTypeProviderTest.java b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/provider/AnnotationActionModuleTypeProviderTest.java index 5505fe94b..e56869f13 100644 --- a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/provider/AnnotationActionModuleTypeProviderTest.java +++ b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/internal/module/provider/AnnotationActionModuleTypeProviderTest.java @@ -41,6 +41,7 @@ import org.openhab.core.automation.type.ActionType; import org.openhab.core.automation.type.Input; import org.openhab.core.automation.type.ModuleType; 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.ParameterOption; import org.openhab.core.test.java.JavaTest; @@ -74,6 +75,7 @@ public class AnnotationActionModuleTypeProviderTest extends JavaTest { private static final String ACTION_OUTPUT2_TYPE = "java.lang.String"; private @Mock @NonNullByDefault({}) ModuleTypeI18nService moduleTypeI18nServiceMock; + private @Mock @NonNullByDefault({}) ActionInputsHelper actionInputsHelperMock; private AnnotatedActions actionProviderConf1 = new TestActionProvider(); private AnnotatedActions actionProviderConf2 = new TestActionProvider(); @@ -86,7 +88,8 @@ public class AnnotationActionModuleTypeProviderTest extends JavaTest { @Test public void testMultiServiceAnnotationActions() { - AnnotatedActionModuleTypeProvider prov = new AnnotatedActionModuleTypeProvider(moduleTypeI18nServiceMock); + AnnotatedActionModuleTypeProvider prov = new AnnotatedActionModuleTypeProvider(moduleTypeI18nServiceMock, + new AnnotationActionModuleTypeHelper(actionInputsHelperMock), actionInputsHelperMock); Map properties1 = Map.of(OpenHAB.SERVICE_CONTEXT, "conf1"); prov.addActionProvider(actionProviderConf1, properties1); diff --git a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProviderTest.java b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProviderTest.java index 72f72ab9f..bed80a27a 100644 --- a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProviderTest.java +++ b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/thingsupport/AnnotatedThingActionModuleTypeProviderTest.java @@ -39,6 +39,7 @@ import org.openhab.core.automation.type.ActionType; import org.openhab.core.automation.type.Input; import org.openhab.core.automation.type.ModuleType; 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.ParameterOption; import org.openhab.core.test.java.JavaTest; @@ -79,6 +80,7 @@ public class AnnotatedThingActionModuleTypeProviderTest extends JavaTest { private static final String ACTION_OUTPUT2_TYPE = "java.lang.String"; private @Mock @NonNullByDefault({}) ModuleTypeI18nService moduleTypeI18nServiceMock; + private @Mock @NonNullByDefault({}) ActionInputsHelper actionInputsHelperMock; private @Mock @NonNullByDefault({}) ThingHandler handler1Mock; private @Mock @NonNullByDefault({}) ThingHandler handler2Mock; @@ -104,7 +106,8 @@ public class AnnotatedThingActionModuleTypeProviderTest extends JavaTest { @Test public void testMultiServiceAnnotationActions() { AnnotatedThingActionModuleTypeProvider prov = new AnnotatedThingActionModuleTypeProvider( - moduleTypeI18nServiceMock); + moduleTypeI18nServiceMock, new AnnotationActionModuleTypeHelper(actionInputsHelperMock), + actionInputsHelperMock); prov.addAnnotatedThingActions(actionProviderConf1); diff --git a/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/util/ActionInputHelperTest.java b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/util/ActionInputHelperTest.java new file mode 100644 index 000000000..f93632a33 --- /dev/null +++ b/bundles/org.openhab.core.automation/src/test/java/org/openhab/core/automation/util/ActionInputHelperTest.java @@ -0,0 +1,556 @@ +/** + * Copyright (c) 2010-2024 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.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.measure.Quantity; +import javax.measure.Unit; +import javax.measure.quantity.Temperature; +import javax.measure.spi.SystemOfUnits; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.Input; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.internal.i18n.I18nProviderImpl; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; + +/** + * @author Laurent Garnier - Initial contribution + */ +@NonNullByDefault +public class ActionInputHelperTest { + private static final String PARAM_NAME = "Param"; + private static final String PARAM_LABEL = "Label Parameter"; + private static final String PARAM_DESCRIPTION = "Description parameter"; + + private UnitProvider unitProvider = new TestUnitProvider(); + private ActionInputsHelper helper = new ActionInputsHelper(unitProvider); + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenBoolean() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Boolean")), + ConfigDescriptionParameter.Type.BOOLEAN, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("boolean")), + ConfigDescriptionParameter.Type.BOOLEAN, true, "false", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenByte() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Byte")), + ConfigDescriptionParameter.Type.INTEGER, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("byte")), + ConfigDescriptionParameter.Type.INTEGER, true, "0", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenShort() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Short")), + ConfigDescriptionParameter.Type.INTEGER, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("short")), + ConfigDescriptionParameter.Type.INTEGER, true, "0", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenInteger() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Integer")), + ConfigDescriptionParameter.Type.INTEGER, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("int")), + ConfigDescriptionParameter.Type.INTEGER, true, "0", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenLong() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Long")), + ConfigDescriptionParameter.Type.INTEGER, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("long")), + ConfigDescriptionParameter.Type.INTEGER, true, "0", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenFloat() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Float")), + ConfigDescriptionParameter.Type.DECIMAL, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("float")), + ConfigDescriptionParameter.Type.DECIMAL, true, "0", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenDouble() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Double")), + ConfigDescriptionParameter.Type.DECIMAL, false, null, null, null); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("double")), + ConfigDescriptionParameter.Type.DECIMAL, true, "0", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenNumber() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.Number")), + ConfigDescriptionParameter.Type.DECIMAL, false, null, null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenDecimalType() { + checkParameter( + helper.mapActionInputToConfigDescriptionParameter( + buildInput("org.openhab.core.library.types.DecimalType")), + ConfigDescriptionParameter.Type.DECIMAL, false, null, null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenQuantityType() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("QuantityType")), + ConfigDescriptionParameter.Type.DECIMAL, false, null, null, "°C"); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenString() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.lang.String")), + ConfigDescriptionParameter.Type.TEXT, false, null, null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenLocalDate() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.time.LocalDate")), + ConfigDescriptionParameter.Type.TEXT, false, null, "date", null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenLocalTime() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.time.LocalTime")), + ConfigDescriptionParameter.Type.TEXT, false, null, "time", null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenLocalDateTime() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.time.LocalDateTime")), + ConfigDescriptionParameter.Type.TEXT, false, null, "datetime", null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenDate() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.util.Date")), + ConfigDescriptionParameter.Type.TEXT, false, null, "datetime", null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenZonedDateTime() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.time.ZonedDateTime")), + ConfigDescriptionParameter.Type.TEXT, false, null, null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenInstant() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.time.Instant")), + ConfigDescriptionParameter.Type.TEXT, false, null, null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenDuration() { + checkParameter(helper.mapActionInputToConfigDescriptionParameter(buildInput("java.time.Duration")), + ConfigDescriptionParameter.Type.TEXT, false, null, null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenDefaultValue() { + Input input = new Input(PARAM_NAME, "int", PARAM_LABEL, PARAM_DESCRIPTION, null, false, null, "-1"); + checkParameter(helper.mapActionInputToConfigDescriptionParameter(input), + ConfigDescriptionParameter.Type.INTEGER, true, "-1", null, null); + } + + @Test + public void testMapActionInputToConfigDescriptionParameterWhenUnsupportedType() { + assertThrows(IllegalArgumentException.class, + () -> helper.mapActionInputToConfigDescriptionParameter(buildInput("List"))); + } + + @Test + public void testMapActionInputsToConfigDescriptionParametersWhenOk() { + Input input1 = buildInput("Boolean", "boolean"); + Input input2 = buildInput("String", "java.lang.String"); + List params = helper + .mapActionInputsToConfigDescriptionParameters(List.of(input1, input2)); + assertThat(params.size(), is(2)); + checkParameter(params.get(0), "Boolean", ConfigDescriptionParameter.Type.BOOLEAN, PARAM_LABEL, + PARAM_DESCRIPTION, true, "false", null, null); + checkParameter(params.get(1), "String", ConfigDescriptionParameter.Type.TEXT, PARAM_LABEL, PARAM_DESCRIPTION, + false, null, null, null); + } + + @Test + public void testMapActionInputsToConfigDescriptionParametersWhenUnsupportedType() { + Input input1 = buildInput("Boolean", "boolean"); + Input input2 = buildInput("List", "List"); + assertThrows(IllegalArgumentException.class, + () -> helper.mapActionInputsToConfigDescriptionParameters(List.of(input1, input2))); + } + + @Test + public void testMapSerializedInputToActionInputWhenBoolean() { + Input input = buildInput("java.lang.Boolean"); + assertThat(helper.mapSerializedInputToActionInput(input, true), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input, false), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input, Boolean.TRUE), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input, Boolean.FALSE), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input, "true"), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input, "True"), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input, "TRUE"), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input, "false"), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input, "False"), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input, "FALSE"), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input, "other"), is(Boolean.FALSE)); + + Input input2 = buildInput("boolean"); + assertThat(helper.mapSerializedInputToActionInput(input2, true), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input2, false), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input2, Boolean.TRUE), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input2, Boolean.FALSE), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "true"), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "True"), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "TRUE"), is(Boolean.TRUE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "false"), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "False"), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "FALSE"), is(Boolean.FALSE)); + assertThat(helper.mapSerializedInputToActionInput(input2, "other"), is(Boolean.FALSE)); + } + + @Test + public void testMapSerializedInputToActionInputWhenByte() { + byte val = 127; + Byte valAsByte = Byte.valueOf(val); + + Input input = buildInput("java.lang.Byte"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(valAsByte)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsByte), is(valAsByte)); + assertThat(helper.mapSerializedInputToActionInput(input, Double.valueOf(val)), is(valAsByte)); + assertThat(helper.mapSerializedInputToActionInput(input, "127"), is(valAsByte)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "128")); + + Input input2 = buildInput("byte"); + assertThat(helper.mapSerializedInputToActionInput(input2, val), is(valAsByte)); + assertThat(helper.mapSerializedInputToActionInput(input2, valAsByte), is(valAsByte)); + assertThat(helper.mapSerializedInputToActionInput(input2, Double.valueOf(val)), is(valAsByte)); + assertThat(helper.mapSerializedInputToActionInput(input2, "127"), is(valAsByte)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input2, "128")); + } + + @Test + public void testMapSerializedInputToActionInputWhenShort() { + short val = 32767; + Short valAsShort = Short.valueOf(val); + + Input input = buildInput("java.lang.Short"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(valAsShort)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsShort), is(valAsShort)); + assertThat(helper.mapSerializedInputToActionInput(input, Double.valueOf(val)), is(valAsShort)); + assertThat(helper.mapSerializedInputToActionInput(input, "32767"), is(valAsShort)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "32768")); + + Input input2 = buildInput("short"); + assertThat(helper.mapSerializedInputToActionInput(input2, val), is(valAsShort)); + assertThat(helper.mapSerializedInputToActionInput(input2, valAsShort), is(valAsShort)); + assertThat(helper.mapSerializedInputToActionInput(input2, Double.valueOf(val)), is(valAsShort)); + assertThat(helper.mapSerializedInputToActionInput(input2, "32767"), is(valAsShort)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input2, "32768")); + } + + @Test + public void testMapSerializedInputToActionInputWhenInteger() { + int val = 123456789; + Integer valAsInteger = Integer.valueOf(val); + + Input input = buildInput("java.lang.Integer"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(valAsInteger)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsInteger), is(valAsInteger)); + assertThat(helper.mapSerializedInputToActionInput(input, Double.valueOf(val)), is(valAsInteger)); + assertThat(helper.mapSerializedInputToActionInput(input, "123456789"), is(valAsInteger)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "wrong")); + + Input input2 = buildInput("int"); + assertThat(helper.mapSerializedInputToActionInput(input2, val), is(valAsInteger)); + assertThat(helper.mapSerializedInputToActionInput(input2, valAsInteger), is(valAsInteger)); + assertThat(helper.mapSerializedInputToActionInput(input2, Double.valueOf(val)), is(valAsInteger)); + assertThat(helper.mapSerializedInputToActionInput(input2, "123456789"), is(valAsInteger)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input2, "wrong")); + } + + @Test + public void testMapSerializedInputToActionInputWhenLong() { + long val = 123456789; + Long valAsLong = Long.valueOf(val); + + Input input = buildInput("java.lang.Long"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(valAsLong)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsLong), is(valAsLong)); + assertThat(helper.mapSerializedInputToActionInput(input, Double.valueOf(val)), is(valAsLong)); + assertThat(helper.mapSerializedInputToActionInput(input, "123456789"), is(valAsLong)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "wrong")); + + Input input2 = buildInput("long"); + assertThat(helper.mapSerializedInputToActionInput(input2, val), is(valAsLong)); + assertThat(helper.mapSerializedInputToActionInput(input2, valAsLong), is(valAsLong)); + assertThat(helper.mapSerializedInputToActionInput(input2, Double.valueOf(val)), is(valAsLong)); + assertThat(helper.mapSerializedInputToActionInput(input2, "123456789"), is(valAsLong)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input2, "wrong")); + } + + @Test + public void testMapSerializedInputToActionInputWhenFloat() { + Float val = 456.789f; + Float valAsFloat = Float.valueOf(val); + + Input input = buildInput("java.lang.Float"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(valAsFloat)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsFloat), is(valAsFloat)); + assertThat(helper.mapSerializedInputToActionInput(input, Double.valueOf(val)), is(valAsFloat)); + assertThat(helper.mapSerializedInputToActionInput(input, "456.789"), is(valAsFloat)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "wrong")); + + Input input2 = buildInput("float"); + assertThat(helper.mapSerializedInputToActionInput(input2, val), is(valAsFloat)); + assertThat(helper.mapSerializedInputToActionInput(input2, valAsFloat), is(valAsFloat)); + assertThat(helper.mapSerializedInputToActionInput(input2, Double.valueOf(val)), is(valAsFloat)); + assertThat(helper.mapSerializedInputToActionInput(input2, "456.789"), is(valAsFloat)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input2, "wrong")); + } + + @Test + public void testMapSerializedInputToActionInputWhenDouble() { + Double val = 456.789d; + Double valAsDouble = Double.valueOf(val); + + Input input = buildInput("java.lang.Double"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(valAsDouble)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsDouble), is(valAsDouble)); + assertThat(helper.mapSerializedInputToActionInput(input, "456.789"), is(valAsDouble)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "wrong")); + + Input input2 = buildInput("double"); + assertThat(helper.mapSerializedInputToActionInput(input2, val), is(valAsDouble)); + assertThat(helper.mapSerializedInputToActionInput(input2, valAsDouble), is(valAsDouble)); + assertThat(helper.mapSerializedInputToActionInput(input2, "456.789"), is(valAsDouble)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input2, "wrong")); + } + + @Test + public void testMapSerializedInputToActionInputWhenDecimalType() { + DecimalType val = new DecimalType(23.45); + Input input = buildInput("org.openhab.core.library.types.DecimalType"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, val.doubleValue()), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, "23.45"), is(val)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "wrong")); + } + + @Test + public void testMapSerializedInputToActionInputWhenQuantityType() { + QuantityType val = new QuantityType<>(19.7, SIUnits.CELSIUS); + Input input = buildInput("QuantityType"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, 19.7d), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, "19.7"), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, "19.7 °C"), is(val)); + assertThrows(IllegalArgumentException.class, () -> helper.mapSerializedInputToActionInput(input, "19.7 XXX")); + } + + @Test + public void testMapSerializedInputToActionInputWhenLocalDate() { + String valAsString = "2024-08-31"; + LocalDate val = LocalDate.parse(valAsString); + Input input = buildInput("java.time.LocalDate"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll("-", " "))); + } + + @Test + public void testMapSerializedInputToActionInputWhenLocalTime() { + String valAsString = "08:30:55"; + LocalTime val = LocalTime.parse(valAsString); + Input input = buildInput("java.time.LocalTime"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll(":", " "))); + } + + @Test + public void testMapSerializedInputToActionInputWhenLocalDateTime() { + String valAsString = "2024-07-01 20:30:45"; + LocalDateTime val = LocalDateTime.parse(valAsString, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + Input input = buildInput("java.time.LocalDateTime"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll(" ", "T"))); + } + + @Test + public void testMapSerializedInputToActionInputWhenZonedDateTime() { + String valAsString = "2007-12-03T10:15:30+01:00[Europe/Paris]"; + ZonedDateTime val = ZonedDateTime.parse(valAsString); + Input input = buildInput("java.time.ZonedDateTime"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll("T", " "))); + } + + @Test + public void testMapSerializedInputToActionInputWhenDate() { + String valAsString = "2024-11-05 09:45:12"; + Date val; + try { + val = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(valAsString); + } catch (IllegalArgumentException | ParseException e) { + val = null; + } + assertNotNull(val); + Input input = buildInput("java.util.Date"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll(" ", "T"))); + } + + @Test + public void testMapSerializedInputToActionInputWhenInstant() { + String valAsString = "2017-12-09T20:15:30.00Z"; + Instant val = Instant.parse(valAsString); + Input input = buildInput("java.time.Instant"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll("T", " "))); + } + + @Test + public void testMapSerializedInputToActionInputWhenDuration() { + String valAsString = "P2DT17H25M30.5S"; + Duration val = Duration.parse(valAsString); + Input input = buildInput("java.time.Duration"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + assertThat(helper.mapSerializedInputToActionInput(input, valAsString), is(val)); + assertThrows(IllegalArgumentException.class, + () -> helper.mapSerializedInputToActionInput(input, valAsString.replaceAll("T", " "))); + } + + @Test + public void testMapSerializedInputToActionInputWhenAnyOtherType() { + List val = List.of("Value 1", "Value 2"); + Input input = buildInput("List"); + assertThat(helper.mapSerializedInputToActionInput(input, val), is(val)); + } + + @Test + public void testMapSerializedInputsToActionInputs() { + Input input1 = buildInput("BooleanParam", "java.lang.Boolean"); + Input input2 = buildInput("DoubleParam", "java.lang.Double"); + Input input3 = buildInput("StringParam", "java.lang.String"); + ActionType action = new ActionType("action", null, List.of(input1, input2, input3)); + + Map result = helper.mapSerializedInputsToActionInputs(action, + Map.of("BooleanParam", true, "OtherParam", "other", "DoubleParam", "invalid", "StringParam", "test")); + assertThat(result.size(), is(2)); + assertThat(result.get("BooleanParam"), is(Boolean.TRUE)); + assertNull(result.get("DoubleParam")); + assertThat(result.get("StringParam"), is("test")); + } + + private Input buildInput(String type) { + return buildInput(PARAM_NAME, type); + } + + private Input buildInput(String name, String type) { + return new Input(name, type, PARAM_LABEL, PARAM_DESCRIPTION, null, false, null, null); + } + + private void checkParameter(ConfigDescriptionParameter param, ConfigDescriptionParameter.Type type, + boolean required, @Nullable String defaultValue, @Nullable String context, @Nullable String unit) { + checkParameter(param, PARAM_NAME, type, PARAM_LABEL, PARAM_DESCRIPTION, required, defaultValue, context, unit); + } + + private void checkParameter(ConfigDescriptionParameter param, String name, ConfigDescriptionParameter.Type type, + String label, String description, boolean required, @Nullable String defaultValue, @Nullable String context, + @Nullable String unit) { + assertThat(param.getName(), is(name)); + assertThat(param.getLabel(), is(label)); + assertThat(param.getDescription(), is(description)); + assertThat(param.getType(), is(type)); + assertThat(param.isReadOnly(), is(false)); + assertThat(param.isRequired(), is(required)); + if (defaultValue == null) { + assertNull(param.getDefault()); + } else { + assertThat(param.getDefault(), is(defaultValue)); + } + if (context == null) { + assertNull(param.getContext()); + } else { + assertThat(param.getContext(), is(context)); + } + if (unit == null) { + assertNull(param.getUnit()); + } else { + assertNotNull(param.getUnit()); + } + } + + public class TestUnitProvider implements UnitProvider { + + private final Map>, Map>>> dimensionMap = I18nProviderImpl + .getDimensionMap(); + + @Override + @SuppressWarnings("unchecked") + public > Unit getUnit(Class dimension) { + return Objects.requireNonNull( + (Unit) dimensionMap.getOrDefault(dimension, Map.of()).get(SIUnits.getInstance())); + } + + @Override + public SystemOfUnits getMeasurementSystem() { + return SIUnits.getInstance(); + } + + @Override + public Collection>> getAllDimensions() { + return Set.of(); + } + } +} diff --git a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java index 8889aeaee..0e4885e2f 100644 --- a/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java +++ b/bundles/org.openhab.core.config.core/src/main/java/org/openhab/core/config/core/ConfigDescriptionParameter.java @@ -37,6 +37,7 @@ import com.google.gson.annotations.SerializedName; * @author Christoph Knauf - Added default constructor, changed Boolean * getter to return primitive types * @author Thomas Höfer - Added unit + * @author Laurent Garnier - Removed constraint on unit value */ public class ConfigDescriptionParameter { @@ -182,9 +183,6 @@ public class ConfigDescriptionParameter { throw new IllegalArgumentException( "Unit or unit label must only be set for integer or decimal configuration parameters"); } - if (unit != null && !UNITS.contains(unit)) { - throw new IllegalArgumentException("The given unit is invalid."); - } this.name = name; this.type = type;