2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.transform.scale.internal;
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.math.BigDecimal;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.Enumeration;
22 import java.util.HashSet;
23 import java.util.LinkedHashMap;
24 import java.util.LinkedHashSet;
25 import java.util.Locale;
27 import java.util.Properties;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 import java.util.stream.Collectors;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.core.common.registry.RegistryChangeListener;
37 import org.openhab.core.config.core.ConfigOptionProvider;
38 import org.openhab.core.config.core.ParameterOption;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.transform.Transformation;
41 import org.openhab.core.transform.TransformationException;
42 import org.openhab.core.transform.TransformationRegistry;
43 import org.openhab.core.transform.TransformationService;
44 import org.osgi.service.component.annotations.Activate;
45 import org.osgi.service.component.annotations.Component;
46 import org.osgi.service.component.annotations.Deactivate;
47 import org.osgi.service.component.annotations.Reference;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The implementation of {@link TransformationService} which transforms the
53 * input by matching it between limits of ranges in a scale file
55 * @author Gaël L'hopital - Initial contribution
56 * @author Markus Rathgeb - drop usage of Guava
58 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = {
59 "openhab.transform=SCALE" })
61 public class ScaleTransformationService
62 implements TransformationService, ConfigOptionProvider, RegistryChangeListener<Transformation> {
64 private final Logger logger = LoggerFactory.getLogger(ScaleTransformationService.class);
66 private static final String PROFILE_CONFIG_URI = "profile:transform:SCALE";
67 private static final String CONFIG_PARAM_FUNCTION = "function";
68 private static final Set<String> SUPPORTED_CONFIGURATION_TYPES = Set.of("scale");
70 /** RegEx to extract a scale definition */
71 private static final Pattern LIMITS_PATTERN = Pattern.compile("(\\[|])(.*)\\.\\.(.*)(\\[|])");
73 private static final String NON_NUMBER = "NaN";
74 private static final String FORMAT = "format";
75 private static final String FORMAT_VALUE = "%value%";
76 private static final String FORMAT_LABEL = "%label%";
78 /** Inaccessible range used to store presentation format ]0..0[ */
79 private static final Range FORMAT_RANGE = Range.range(BigDecimal.ZERO, false, BigDecimal.ZERO, false);
80 private final TransformationRegistry transformationRegistry;
82 private final Map<String, Map<@Nullable Range, String>> cachedTransformations = new ConcurrentHashMap<>();
85 public ScaleTransformationService(@Reference TransformationRegistry transformationRegistry) {
86 this.transformationRegistry = transformationRegistry;
87 transformationRegistry.addRegistryChangeListener(this);
91 public void deactivate() {
92 transformationRegistry.removeRegistryChangeListener(this);
96 public void added(Transformation element) {
97 // do nothing, configurations are added to cache if needed
101 public void removed(Transformation element) {
102 cachedTransformations.remove(element.getUID());
106 public void updated(Transformation oldElement, Transformation element) {
107 if (cachedTransformations.remove(oldElement.getUID()) != null) {
108 // import only if it was present before
109 importConfiguration(element);
114 * The implementation of {@link OrderedProperties} that let access
115 * properties in the same order than presented in the source file
116 * by using the orderedKeys function.
118 * This implementation is limited to the sole purpose of the class
119 * (e.g. it does not handle removing elements)
121 * @author Gaël L'hopital
123 static class OrderedProperties extends Properties {
124 private static final long serialVersionUID = 3860553217028220119L;
125 private final HashSet<@Nullable Object> keys = new LinkedHashSet<>();
127 Set<@Nullable Object> orderedKeys() {
132 public @NonNullByDefault({}) Enumeration<Object> keys() {
133 return Collections.enumeration(keys);
137 public @Nullable Object put(@Nullable Object key, @Nullable Object value) {
139 return super.put(key, value);
144 public @Nullable String transform(String function, String source) throws TransformationException {
145 // always get a configuration from the registry to account for changed system locale
146 Transformation transformation = transformationRegistry.get(function, null);
148 if (transformation != null) {
149 if (!cachedTransformations.containsKey(transformation.getUID())) {
150 importConfiguration(transformation);
152 Map<@Nullable Range, String> data = cachedTransformations.get(transformation.getUID());
158 final BigDecimal value = new BigDecimal(source);
159 target = formatResult(data, source, value);
160 } catch (NumberFormatException e) {
161 // Scale can only be used with numeric inputs, so lets try to see if ever its a valid quantity type
163 final QuantityType<?> quantity = new QuantityType<>(source);
164 return formatResult(data, source, quantity.toBigDecimal());
165 } catch (IllegalArgumentException e2) {
166 String nonNumeric = data.get(null);
167 if (nonNumeric != null) {
170 throw new TransformationException(
171 "Scale must be used with numeric inputs, valid quantity types or a 'NaN' entry.");
175 logger.debug("Transformation resulted in '{}'", target);
180 throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
183 private String formatResult(Map<@Nullable Range, String> data, String source, final BigDecimal value)
184 throws TransformationException {
185 String format = data.get(FORMAT_RANGE);
186 String result = getScaleResult(data, source, value);
187 return format.replaceAll(FORMAT_VALUE, source).replaceAll(FORMAT_LABEL, result);
190 private String getScaleResult(Map<@Nullable Range, String> data, String source, final BigDecimal value)
191 throws TransformationException {
192 return data.entrySet().stream().filter(entry -> entry.getKey() != null && entry.getKey().contains(value))
193 .findFirst().map(Map.Entry::getValue)
194 .orElseThrow(() -> new TransformationException("No matching range for '" + source + "'"));
197 private void importConfiguration(@Nullable Transformation configuration) {
198 if (configuration != null) {
200 final Map<@Nullable Range, String> data = new LinkedHashMap<>();
201 data.put(FORMAT_RANGE, FORMAT_LABEL);
202 final OrderedProperties properties = new OrderedProperties();
203 String function = configuration.getConfiguration().get(Transformation.FUNCTION);
204 if (function == null) {
207 properties.load(new StringReader(function));
209 for (Object orderedKey : properties.orderedKeys()) {
210 final String entry = (String) orderedKey;
211 final String value = properties.getProperty(entry);
212 final Matcher matcher = LIMITS_PATTERN.matcher(entry);
213 if (matcher.matches() && (matcher.groupCount() == 4)) {
214 final boolean lowerInclusive = "[".equals(matcher.group(1));
215 final boolean upperInclusive = "]".equals(matcher.group(4));
217 final String lowLimit = matcher.group(2);
218 final String highLimit = matcher.group(3);
220 final BigDecimal lowValue = lowLimit.isEmpty() ? null : new BigDecimal(lowLimit);
221 final BigDecimal highValue = highLimit.isEmpty() ? null : new BigDecimal(highLimit);
222 final Range range = Range.range(lowValue, lowerInclusive, highValue, upperInclusive);
224 data.put(range, value);
226 if (NON_NUMBER.equals(entry)) {
227 data.put(null, value);
228 } else if (FORMAT.equals(entry)) {
229 data.put(FORMAT_RANGE, value);
232 "Scale transformation configuration '{}' does not comply with syntax for entry : '{}', '{}'",
233 configuration.getUID(), entry, value);
238 cachedTransformations.put(configuration.getUID(), data);
239 } catch (IOException | NumberFormatException ignored) {
245 public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
246 @Nullable Locale locale) {
247 if (PROFILE_CONFIG_URI.equals(uri.toString())) {
248 if (CONFIG_PARAM_FUNCTION.equals(param)) {
249 return transformationRegistry.getTransformations(SUPPORTED_CONFIGURATION_TYPES).stream()
250 .map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());