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