]> git.basschouten.com Git - openhab-addons.git/blob
b7cf56aceaa7f3fe4aba289445f2b4fd181280a0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.transform.scale.internal;
14
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.math.BigDecimal;
18 import java.net.URI;
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;
26 import java.util.Map;
27 import java.util.Properties;
28 import java.util.Set;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 import java.util.stream.Collectors;
33
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;
50
51 /**
52  * The implementation of {@link TransformationService} which transforms the
53  * input by matching it between limits of ranges in a scale file
54  *
55  * @author Gaël L'hopital
56  * @author Markus Rathgeb - drop usage of Guava
57  */
58 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = {
59         "openhab.transform=SCALE" })
60 @NonNullByDefault
61 public class ScaleTransformationService
62         implements TransformationService, ConfigOptionProvider, RegistryChangeListener<Transformation> {
63
64     private final Logger logger = LoggerFactory.getLogger(ScaleTransformationService.class);
65
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");
69
70     /** RegEx to extract a scale definition */
71     private static final Pattern LIMITS_PATTERN = Pattern.compile("(\\[|])(.*)\\.\\.(.*)(\\[|])");
72
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%";
77
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;
81
82     private final Map<String, Map<@Nullable Range, String>> cachedTransformations = new ConcurrentHashMap<>();
83
84     @Activate
85     public ScaleTransformationService(@Reference TransformationRegistry transformationRegistry) {
86         this.transformationRegistry = transformationRegistry;
87         transformationRegistry.addRegistryChangeListener(this);
88     }
89
90     @Deactivate
91     public void deactivate() {
92         transformationRegistry.removeRegistryChangeListener(this);
93     }
94
95     @Override
96     public void added(Transformation element) {
97         // do nothing, configurations are added to cache if needed
98     }
99
100     @Override
101     public void removed(Transformation element) {
102         cachedTransformations.remove(element.getUID());
103     }
104
105     @Override
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);
110         }
111     }
112
113     /**
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.
117      *
118      * This implementation is limited to the sole purpose of the class
119      * (e.g. it does not handle removing elements)
120      *
121      * @author Gaël L'hopital
122      */
123     static class OrderedProperties extends Properties {
124         private static final long serialVersionUID = 3860553217028220119L;
125         private final HashSet<@Nullable Object> keys = new LinkedHashSet<>();
126
127         Set<@Nullable Object> orderedKeys() {
128             return keys;
129         }
130
131         @Override
132         public @NonNullByDefault({}) Enumeration<Object> keys() {
133             return Collections.enumeration(keys);
134         }
135
136         @Override
137         public @Nullable Object put(@Nullable Object key, @Nullable Object value) {
138             keys.add(key);
139             return super.put(key, value);
140         }
141     }
142
143     @Override
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);
147
148         if (transformation != null) {
149             if (!cachedTransformations.containsKey(transformation.getUID())) {
150                 importConfiguration(transformation);
151             }
152             Map<@Nullable Range, String> data = cachedTransformations.get(transformation.getUID());
153
154             if (data != null) {
155                 String target;
156
157                 try {
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
162                     try {
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) {
168                             target = nonNumeric;
169                         } else {
170                             throw new TransformationException(
171                                     "Scale must be used with numeric inputs, valid quantity types or a 'NaN' entry.");
172                         }
173                     }
174                 }
175                 logger.debug("Transformation resulted in '{}'", target);
176                 return target;
177             }
178         }
179
180         throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
181     }
182
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);
188     }
189
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 + "'"));
195     }
196
197     private void importConfiguration(@Nullable Transformation configuration) {
198         if (configuration != null) {
199             try {
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) {
205                     return;
206                 }
207                 properties.load(new StringReader(function));
208
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 = matcher.group(1).equals("[");
215                         final boolean upperInclusive = matcher.group(4).equals("]");
216
217                         final String lowLimit = matcher.group(2);
218                         final String highLimit = matcher.group(3);
219
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);
223
224                         data.put(range, value);
225                     } else {
226                         if (NON_NUMBER.equals(entry)) {
227                             data.put(null, value);
228                         } else if (FORMAT.equals(entry)) {
229                             data.put(FORMAT_RANGE, value);
230                         } else {
231                             logger.warn(
232                                     "Scale transformation configuration '{}' does not comply with syntax for entry : '{}', '{}'",
233                                     configuration.getUID(), entry, value);
234                         }
235                     }
236                 }
237
238                 cachedTransformations.put(configuration.getUID(), data);
239             } catch (IOException | NumberFormatException ignored) {
240             }
241         }
242     }
243
244     @Override
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());
251             }
252         }
253         return null;
254     }
255 }