2 * Copyright (c) 2010-2020 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;
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;
24 import java.util.Properties;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
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;
38 * The implementation of {@link TransformationService} which transforms the
39 * input by matching it between limits of ranges in a scale file
41 * @author Gaël L'hopital
42 * @author Markus Rathgeb - drop usage of Guava
44 @Component(immediate = true, service = TransformationService.class, property = { "smarthome.transform=SCALE" })
45 public class ScaleTransformationService extends AbstractFileTransformationService<Map<Range, String>> {
47 private final Logger logger = LoggerFactory.getLogger(ScaleTransformationService.class);
49 /** RegEx to extract a scale definition */
50 private static final Pattern LIMITS_PATTERN = Pattern.compile("(\\[|\\])(.*)\\.\\.(.*)(\\[|\\])");
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%";
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);
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.
65 * This implementation is limited to the sole purpose of the class
66 * (e.g. it does not handle removing elements)
68 * @author Gaël L'hopital
70 static class OrderedProperties extends Properties {
71 private static final long serialVersionUID = 3860553217028220119L;
72 private final HashSet<Object> keys = new LinkedHashSet<>();
74 Set<Object> orderedKeys() {
79 public Enumeration<Object> keys() {
80 return Collections.<Object> enumeration(keys);
84 public Object put(Object key, Object value) {
86 return super.put(key, value);
91 * Performs transformation of the input <code>source</code>
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
96 * @param properties the list of properties defining all the available ranges
97 * @param source the input to transform
101 protected String internalTransform(Map<Range, String> data, String source) throws TransformationException {
103 final BigDecimal value = new BigDecimal(source);
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
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) {
116 throw new TransformationException(
117 "Scale must be used with numeric inputs, valid quantity types or a 'NaN' entry.");
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);
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 + "'"));
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);
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("]");
153 final String lowLimit = matcher.group(2);
154 final String highLimit = matcher.group(3);
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);
161 data.put(range, value);
162 } catch (NumberFormatException ex) {
163 throw new TransformationException("Error parsing bounds: " + lowLimit + ".." + highLimit);
166 if (NON_NUMBER.equals(entry)) {
167 data.put(null, value);
168 } else if (FORMAT.equals(entry)) {
169 data.put(FORMAT_RANGE, value);
171 logger.warn("Scale transform file '{}' does not comply with syntax for entry : '{}', '{}'",
172 filename, entry, value);
178 } catch (final IOException ex) {
179 throw new TransformationException("An error occurred while opening file.", ex);