2 * Copyright (c) 2010-2021 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.FileReader;
16 import java.io.IOException;
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.regex.Matcher;
30 import java.util.regex.Pattern;
31 import java.util.stream.Collectors;
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;
46 * The implementation of {@link TransformationService} which transforms the
47 * input by matching it between limits of ranges in a scale file
49 * @author Gaël L'hopital
50 * @author Markus Rathgeb - drop usage of Guava
52 @Component(service = { TransformationService.class, ConfigOptionProvider.class }, property = {
53 "openhab.transform=SCALE" })
54 public class ScaleTransformationService extends AbstractFileTransformationService<Map<Range, String>>
55 implements ConfigOptionProvider {
57 private final Logger logger = LoggerFactory.getLogger(ScaleTransformationService.class);
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" };
63 /** RegEx to extract a scale definition */
64 private static final Pattern LIMITS_PATTERN = Pattern.compile("(\\[|\\])(.*)\\.\\.(.*)(\\[|\\])");
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%";
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);
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.
79 * This implementation is limited to the sole purpose of the class
80 * (e.g. it does not handle removing elements)
82 * @author Gaël L'hopital
84 static class OrderedProperties extends Properties {
85 private static final long serialVersionUID = 3860553217028220119L;
86 private final HashSet<Object> keys = new LinkedHashSet<>();
88 Set<Object> orderedKeys() {
93 public Enumeration<Object> keys() {
94 return Collections.<Object> enumeration(keys);
98 public Object put(Object key, Object value) {
100 return super.put(key, value);
105 * Performs transformation of the input <code>source</code>
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
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.
115 protected @Nullable String internalTransform(Map<Range, String> data, String source)
116 throws TransformationException {
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
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) {
130 throw new TransformationException(
131 "Scale must be used with numeric inputs, valid quantity types or a 'NaN' entry.");
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);
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 + "'"));
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);
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("]");
167 final String lowLimit = matcher.group(2);
168 final String highLimit = matcher.group(3);
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);
175 data.put(range, value);
176 } catch (NumberFormatException ex) {
177 throw new TransformationException("Error parsing bounds: " + lowLimit + ".." + highLimit);
180 if (NON_NUMBER.equals(entry)) {
181 data.put(null, value);
182 } else if (FORMAT.equals(entry)) {
183 data.put(FORMAT_RANGE, value);
185 logger.warn("Scale transform file '{}' does not comply with syntax for entry : '{}', '{}'",
186 filename, entry, value);
192 } catch (final IOException ex) {
193 throw new TransformationException("An error occurred while opening file.", ex);
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())) {
202 case CONFIG_PARAM_FUNCTION:
203 return getFilenames(FILE_NAME_EXTENSIONS).stream().map(f -> new ParameterOption(f, f))
204 .collect(Collectors.toList());