2 * Copyright (c) 2010-2022 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.binding.mqtt.generic.values;
15 import java.math.BigDecimal;
16 import java.math.MathContext;
17 import java.util.stream.Collectors;
18 import java.util.stream.Stream;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.core.library.CoreItemFactory;
23 import org.openhab.core.library.types.DecimalType;
24 import org.openhab.core.library.types.IncreaseDecreaseType;
25 import org.openhab.core.library.types.OnOffType;
26 import org.openhab.core.library.types.PercentType;
27 import org.openhab.core.library.types.QuantityType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.library.types.UpDownType;
30 import org.openhab.core.library.unit.Units;
31 import org.openhab.core.types.Command;
32 import org.openhab.core.types.StateDescriptionFragmentBuilder;
33 import org.openhab.core.types.UnDefType;
36 * Implements a percentage value. Minimum and maximum are definable.
39 * Accepts user updates from a DecimalType, IncreaseDecreaseType and UpDownType.
40 * If this is a percent value, PercentType
42 * Accepts MQTT state updates as DecimalType, IncreaseDecreaseType and UpDownType
43 * StringType with comma separated HSB ("h,s,b"), RGB ("r,g,b") and on, off strings.
44 * On, Off strings can be customized.
46 * @author David Graeff - Initial contribution
49 public class PercentageValue extends Value {
50 private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
51 private final BigDecimal min;
52 private final BigDecimal max;
53 private final BigDecimal span;
54 private final BigDecimal step;
55 private final BigDecimal stepPercent;
56 private final @Nullable String onValue;
57 private final @Nullable String offValue;
59 public PercentageValue(@Nullable BigDecimal min, @Nullable BigDecimal max, @Nullable BigDecimal step,
60 @Nullable String onValue, @Nullable String offValue) {
61 super(CoreItemFactory.DIMMER, Stream.of(DecimalType.class, QuantityType.class, IncreaseDecreaseType.class,
62 OnOffType.class, UpDownType.class, StringType.class).collect(Collectors.toList()));
63 this.onValue = onValue;
64 this.offValue = offValue;
65 this.min = min == null ? BigDecimal.ZERO : min;
66 this.max = max == null ? HUNDRED : max;
67 if (this.min.compareTo(this.max) >= 0) {
68 throw new IllegalArgumentException("Min need to be smaller than max!");
70 this.span = this.max.subtract(this.min);
71 this.step = step == null ? BigDecimal.ONE : step;
72 this.stepPercent = this.step.multiply(HUNDRED).divide(this.span, MathContext.DECIMAL128);
76 public void update(Command command) throws IllegalArgumentException {
77 PercentType oldvalue = (state == UnDefType.UNDEF) ? new PercentType() : (PercentType) state;
78 // Nothing do to -> We have received a percentage
79 if (command instanceof PercentType) {
80 state = (PercentType) command;
82 // A decimal type need to be converted according to the current min/max values
83 if (command instanceof DecimalType) {
84 BigDecimal v = ((DecimalType) command).toBigDecimal();
85 v = v.subtract(min).multiply(HUNDRED).divide(max.subtract(min), MathContext.DECIMAL128);
86 state = new PercentType(v);
88 // A quantity type need to be converted according to the current min/max values
89 if (command instanceof QuantityType) {
90 QuantityType<?> qty = ((QuantityType<?>) command).toUnit(Units.PERCENT);
92 BigDecimal v = qty.toBigDecimal();
93 v = v.subtract(min).multiply(HUNDRED).divide(max.subtract(min), MathContext.DECIMAL128);
94 state = new PercentType(v);
97 // Increase or decrease by "step"
98 if (command instanceof IncreaseDecreaseType) {
99 if (((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE) {
100 final BigDecimal v = oldvalue.toBigDecimal().add(stepPercent);
101 state = v.compareTo(HUNDRED) <= 0 ? new PercentType(v) : PercentType.HUNDRED;
103 final BigDecimal v = oldvalue.toBigDecimal().subtract(stepPercent);
104 state = v.compareTo(BigDecimal.ZERO) >= 0 ? new PercentType(v) : PercentType.ZERO;
107 // On/Off equals 100 or 0 percent
108 if (command instanceof OnOffType) {
109 state = ((OnOffType) command) == OnOffType.ON ? PercentType.HUNDRED : PercentType.ZERO;
111 // Increase or decrease by "step"
112 if (command instanceof UpDownType) {
113 if (((UpDownType) command) == UpDownType.UP) {
114 final BigDecimal v = oldvalue.toBigDecimal().add(stepPercent);
115 state = v.compareTo(HUNDRED) <= 0 ? new PercentType(v) : PercentType.HUNDRED;
117 final BigDecimal v = oldvalue.toBigDecimal().subtract(stepPercent);
118 state = v.compareTo(BigDecimal.ZERO) >= 0 ? new PercentType(v) : PercentType.ZERO;
121 // Check against custom on/off values
122 if (command instanceof StringType) {
123 if (onValue != null && command.toString().equals(onValue)) {
124 state = new PercentType(max);
125 } else if (offValue != null && command.toString().equals(offValue)) {
126 state = new PercentType(min);
128 throw new IllegalStateException("Unknown String!");
131 // We are desperate -> Try to parse the command as number value
132 state = PercentType.valueOf(command.toString());
137 public String getMQTTpublishValue(@Nullable String pattern) {
138 if (state == UnDefType.UNDEF) {
141 // Formula: From percentage to custom min/max: value*span/100+min
142 // Calculation need to happen with big decimals to either return a straight integer or a decimal depending on
144 BigDecimal value = ((PercentType) state).toBigDecimal().multiply(span).divide(HUNDRED, MathContext.DECIMAL128)
145 .add(min).stripTrailingZeros();
147 String formatPattern = pattern;
148 if (formatPattern == null) {
149 formatPattern = "%s";
152 return new DecimalType(value).format(formatPattern);
156 public StateDescriptionFragmentBuilder createStateDescription(boolean readOnly) {
157 return super.createStateDescription(readOnly).withMaximum(HUNDRED).withMinimum(BigDecimal.ZERO).withStep(step)
158 .withPattern("%s %%");