]> git.basschouten.com Git - openhab-addons.git/blob
48b422b6b69b6ec14c954b605e18a5508158517d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.TransformationConfiguration;
41 import org.openhab.core.transform.TransformationConfigurationRegistry;
42 import org.openhab.core.transform.TransformationException;
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<TransformationConfiguration> {
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 TransformationConfigurationRegistry transformationConfigurationRegistry;
81
82     private final Map<String, Map<@Nullable Range, String>> cachedTransformations = new ConcurrentHashMap<>();
83
84     @Activate
85     public ScaleTransformationService(
86             @Reference TransformationConfigurationRegistry transformationConfigurationRegistry) {
87         this.transformationConfigurationRegistry = transformationConfigurationRegistry;
88         transformationConfigurationRegistry.addRegistryChangeListener(this);
89     }
90
91     @Deactivate
92     public void deactivate() {
93         transformationConfigurationRegistry.removeRegistryChangeListener(this);
94     }
95
96     @Override
97     public void added(TransformationConfiguration element) {
98         // do nothing, configurations are added to cache if needed
99     }
100
101     @Override
102     public void removed(TransformationConfiguration element) {
103         cachedTransformations.remove(element.getUID());
104     }
105
106     @Override
107     public void updated(TransformationConfiguration oldElement, TransformationConfiguration element) {
108         if (cachedTransformations.remove(oldElement.getUID()) != null) {
109             // import only if it was present before
110             importConfiguration(element);
111         }
112     }
113
114     /**
115      * The implementation of {@link OrderedProperties} that let access
116      * properties in the same order than presented in the source file
117      * by using the orderedKeys function.
118      *
119      * This implementation is limited to the sole purpose of the class
120      * (e.g. it does not handle removing elements)
121      *
122      * @author Gaël L'hopital
123      */
124     static class OrderedProperties extends Properties {
125         private static final long serialVersionUID = 3860553217028220119L;
126         private final HashSet<@Nullable Object> keys = new LinkedHashSet<>();
127
128         Set<@Nullable Object> orderedKeys() {
129             return keys;
130         }
131
132         @Override
133         public @NonNullByDefault({}) Enumeration<Object> keys() {
134             return Collections.enumeration(keys);
135         }
136
137         @Override
138         public @Nullable Object put(@Nullable Object key, @Nullable Object value) {
139             keys.add(key);
140             return super.put(key, value);
141         }
142     }
143
144     @Override
145     public @Nullable String transform(String function, String source) throws TransformationException {
146         // always get a configuration from the registry to account for changed system locale
147         TransformationConfiguration transformationConfiguration = transformationConfigurationRegistry.get(function,
148                 null);
149
150         if (transformationConfiguration != null) {
151             if (!cachedTransformations.containsKey(transformationConfiguration.getUID())) {
152                 importConfiguration(transformationConfiguration);
153             }
154             Map<@Nullable Range, String> data = cachedTransformations.get(function);
155
156             if (data != null) {
157                 String target;
158
159                 try {
160                     final BigDecimal value = new BigDecimal(source);
161                     target = formatResult(data, source, value);
162                 } catch (NumberFormatException e) {
163                     // Scale can only be used with numeric inputs, so lets try to see if ever its a valid quantity type
164                     try {
165                         final QuantityType<?> quantity = new QuantityType<>(source);
166                         return formatResult(data, source, quantity.toBigDecimal());
167                     } catch (IllegalArgumentException e2) {
168                         String nonNumeric = data.get(null);
169                         if (nonNumeric != null) {
170                             target = nonNumeric;
171                         } else {
172                             throw new TransformationException(
173                                     "Scale must be used with numeric inputs, valid quantity types or a 'NaN' entry.");
174                         }
175                     }
176                 }
177                 logger.debug("Transformation resulted in '{}'", target);
178                 return target;
179             }
180         }
181
182         throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
183     }
184
185     private String formatResult(Map<@Nullable Range, String> data, String source, final BigDecimal value)
186             throws TransformationException {
187         String format = data.get(FORMAT_RANGE);
188         String result = getScaleResult(data, source, value);
189         return format.replaceAll(FORMAT_VALUE, source).replaceAll(FORMAT_LABEL, result);
190     }
191
192     private String getScaleResult(Map<@Nullable Range, String> data, String source, final BigDecimal value)
193             throws TransformationException {
194         return data.entrySet().stream().filter(entry -> entry.getKey() != null && entry.getKey().contains(value))
195                 .findFirst().map(Map.Entry::getValue)
196                 .orElseThrow(() -> new TransformationException("No matching range for '" + source + "'"));
197     }
198
199     private void importConfiguration(@Nullable TransformationConfiguration configuration) {
200         if (configuration != null) {
201             try {
202                 final Map<@Nullable Range, String> data = new LinkedHashMap<>();
203                 data.put(FORMAT_RANGE, FORMAT_LABEL);
204                 final OrderedProperties properties = new OrderedProperties();
205                 properties.load(new StringReader(configuration.getContent()));
206
207                 for (Object orderedKey : properties.orderedKeys()) {
208                     final String entry = (String) orderedKey;
209                     final String value = properties.getProperty(entry);
210                     final Matcher matcher = LIMITS_PATTERN.matcher(entry);
211                     if (matcher.matches() && (matcher.groupCount() == 4)) {
212                         final boolean lowerInclusive = matcher.group(1).equals("[");
213                         final boolean upperInclusive = matcher.group(4).equals("]");
214
215                         final String lowLimit = matcher.group(2);
216                         final String highLimit = matcher.group(3);
217
218                         final BigDecimal lowValue = lowLimit.isEmpty() ? null : new BigDecimal(lowLimit);
219                         final BigDecimal highValue = highLimit.isEmpty() ? null : new BigDecimal(highLimit);
220                         final Range range = Range.range(lowValue, lowerInclusive, highValue, upperInclusive);
221
222                         data.put(range, value);
223                     } else {
224                         if (NON_NUMBER.equals(entry)) {
225                             data.put(null, value);
226                         } else if (FORMAT.equals(entry)) {
227                             data.put(FORMAT_RANGE, value);
228                         } else {
229                             logger.warn(
230                                     "Scale transformation configuration '{}' does not comply with syntax for entry : '{}', '{}'",
231                                     configuration.getUID(), entry, value);
232                         }
233                     }
234                 }
235
236                 cachedTransformations.put(configuration.getUID(), data);
237             } catch (IOException | NumberFormatException ignored) {
238             }
239         }
240     }
241
242     @Override
243     public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
244             @Nullable Locale locale) {
245         if (PROFILE_CONFIG_URI.equals(uri.toString())) {
246             if (CONFIG_PARAM_FUNCTION.equals(param)) {
247                 return transformationConfigurationRegistry.getConfigurations(SUPPORTED_CONFIGURATION_TYPES).stream()
248                         .map(c -> new ParameterOption(c.getUID(), c.getLabel())).collect(Collectors.toList());
249             }
250         }
251         return null;
252     }
253 }