]> git.basschouten.com Git - openhab-addons.git/blob
588a103f579f392c33f759b96b80d2ce0e4f2c29
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.FileReader;
16 import java.io.IOException;
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.regex.Matcher;
30 import java.util.regex.Pattern;
31 import java.util.stream.Collectors;
32
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.core.config.core.ConfigOptionProvider;
36 import org.openhab.core.config.core.ParameterOption;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.transform.AbstractFileTransformationService;
39 import org.openhab.core.transform.TransformationException;
40 import org.openhab.core.transform.TransformationService;
41 import org.osgi.service.component.annotations.Component;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The implementation of {@link TransformationService} which transforms the
47  * input by matching it between limits of ranges in a scale file
48  *
49  * @author Gaël L'hopital
50  * @author Markus Rathgeb - drop usage of Guava
51  */
52 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = {
53         "openhab.transform=SCALE" })
54 public class ScaleTransformationService extends AbstractFileTransformationService<Map<Range, String>>
55         implements ConfigOptionProvider {
56
57     private final Logger logger = LoggerFactory.getLogger(ScaleTransformationService.class);
58
59     private static final String PROFILE_CONFIG_URI = "profile:transform:SCALE";
60     private static final String CONFIG_PARAM_FUNCTION = "function";
61     private static final String[] FILE_NAME_EXTENSIONS = { "scale" };
62
63     /** RegEx to extract a scale definition */
64     private static final Pattern LIMITS_PATTERN = Pattern.compile("(\\[|\\])(.*)\\.\\.(.*)(\\[|\\])");
65
66     private static final String NON_NUMBER = "NaN";
67     private static final String FORMAT = "format";
68     private static final String FORMAT_VALUE = "%value%";
69     private static final String FORMAT_LABEL = "%label%";
70
71     /** Inaccessible range used to store presentation format ]0..0[ */
72     private static final Range FORMAT_RANGE = Range.range(BigDecimal.ZERO, false, BigDecimal.ZERO, false);
73
74     /**
75      * The implementation of {@link OrderedProperties} that let access
76      * properties in the same order than presented in the source file
77      * by using the orderedKeys function.
78      *
79      * This implementation is limited to the sole purpose of the class
80      * (e.g. it does not handle removing elements)
81      *
82      * @author Gaël L'hopital
83      */
84     static class OrderedProperties extends Properties {
85         private static final long serialVersionUID = 3860553217028220119L;
86         private final HashSet<Object> keys = new LinkedHashSet<>();
87
88         Set<Object> orderedKeys() {
89             return keys;
90         }
91
92         @Override
93         public Enumeration<Object> keys() {
94             return Collections.<Object> enumeration(keys);
95         }
96
97         @Override
98         public Object put(Object key, Object value) {
99             keys.add(key);
100             return super.put(key, value);
101         }
102     }
103
104     /**
105      * Performs transformation of the input <code>source</code>
106      *
107      * The method transforms the input <code>source</code> by matching searching
108      * the range where it fits i.e. [min..max]=value or ]min..max]=value
109      *
110      * @param properties the list of properties defining all the available ranges
111      * @param source the input to transform
112      * @return the transformed result or null if the transformation couldn't be completed for any reason.
113      */
114     @Override
115     protected @Nullable String internalTransform(Map<Range, String> data, String source)
116             throws TransformationException {
117         try {
118             final BigDecimal value = new BigDecimal(source);
119             return formatResult(data, source, value);
120         } catch (NumberFormatException e) {
121             // Scale can only be used with numeric inputs, so lets try to see if ever its a valid quantity type
122             try {
123                 final QuantityType<?> quantity = new QuantityType<>(source);
124                 return formatResult(data, source, quantity.toBigDecimal());
125             } catch (IllegalArgumentException e2) {
126                 String nonNumeric = data.get(null);
127                 if (nonNumeric != null) {
128                     return nonNumeric;
129                 } else {
130                     throw new TransformationException(
131                             "Scale must be used with numeric inputs, valid quantity types or a 'NaN' entry.");
132                 }
133             }
134         }
135     }
136
137     private String formatResult(Map<Range, String> data, String source, final BigDecimal value)
138             throws TransformationException {
139         String format = data.get(FORMAT_RANGE);
140         String result = getScaleResult(data, source, value);
141         return format.replaceAll(FORMAT_VALUE, source).replaceAll(FORMAT_LABEL, result);
142     }
143
144     private String getScaleResult(Map<Range, String> data, String source, final BigDecimal value)
145             throws TransformationException {
146         return data.entrySet().stream().filter(entry -> entry.getKey() != null && entry.getKey().contains(value))
147                 .findFirst().map(Map.Entry::getValue)
148                 .orElseThrow(() -> new TransformationException("No matching range for '" + source + "'"));
149     }
150
151     @Override
152     protected Map<Range, String> internalLoadTransform(String filename) throws TransformationException {
153         try (FileReader reader = new FileReader(filename)) {
154             final Map<Range, String> data = new LinkedHashMap<>();
155             data.put(FORMAT_RANGE, FORMAT_LABEL);
156             final OrderedProperties properties = new OrderedProperties();
157             properties.load(reader);
158
159             for (Object orderedKey : properties.orderedKeys()) {
160                 final String entry = (String) orderedKey;
161                 final String value = properties.getProperty(entry);
162                 final Matcher matcher = LIMITS_PATTERN.matcher(entry);
163                 if (matcher.matches() && (matcher.groupCount() == 4)) {
164                     final boolean lowerInclusive = matcher.group(1).equals("[");
165                     final boolean upperInclusive = matcher.group(4).equals("]");
166
167                     final String lowLimit = matcher.group(2);
168                     final String highLimit = matcher.group(3);
169
170                     try {
171                         final BigDecimal lowValue = lowLimit.isEmpty() ? null : new BigDecimal(lowLimit);
172                         final BigDecimal highValue = highLimit.isEmpty() ? null : new BigDecimal(highLimit);
173                         final Range range = Range.range(lowValue, lowerInclusive, highValue, upperInclusive);
174
175                         data.put(range, value);
176                     } catch (NumberFormatException ex) {
177                         throw new TransformationException("Error parsing bounds: " + lowLimit + ".." + highLimit);
178                     }
179                 } else {
180                     if (NON_NUMBER.equals(entry)) {
181                         data.put(null, value);
182                     } else if (FORMAT.equals(entry)) {
183                         data.put(FORMAT_RANGE, value);
184                     } else {
185                         logger.warn("Scale transform file '{}' does not comply with syntax for entry : '{}', '{}'",
186                                 filename, entry, value);
187                     }
188                 }
189             }
190
191             return data;
192         } catch (final IOException ex) {
193             throw new TransformationException("An error occurred while opening file.", ex);
194         }
195     }
196
197     @Override
198     public @Nullable Collection<@NonNull ParameterOption> getParameterOptions(URI uri, String param,
199             @Nullable String context, @Nullable Locale locale) {
200         if (PROFILE_CONFIG_URI.equals(uri.toString())) {
201             switch (param) {
202                 case CONFIG_PARAM_FUNCTION:
203                     return getFilenames(FILE_NAME_EXTENSIONS).stream().map(f -> new ParameterOption(f, f))
204                             .collect(Collectors.toList());
205             }
206         }
207         return null;
208     }
209 }