]> git.basschouten.com Git - openhab-addons.git/blob
cabddd0aecc6c3c60599595aae81c4d2dd2d48af
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.binding.modbus.internal.profiles;
14
15 import java.math.BigDecimal;
16 import java.util.Optional;
17
18 import javax.measure.Quantity;
19 import javax.measure.UnconvertibleException;
20 import javax.measure.Unit;
21 import javax.measure.quantity.Dimensionless;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.library.types.DecimalType;
26 import org.openhab.core.library.types.QuantityType;
27 import org.openhab.core.library.unit.Units;
28 import org.openhab.core.thing.profiles.ProfileCallback;
29 import org.openhab.core.thing.profiles.ProfileContext;
30 import org.openhab.core.thing.profiles.ProfileTypeUID;
31 import org.openhab.core.thing.profiles.StateProfile;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.RefreshType;
34 import org.openhab.core.types.State;
35 import org.openhab.core.types.Type;
36 import org.openhab.core.types.UnDefType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * Profile for applying gain and offset to values.
42  *
43  * Output of the profile is
44  * - (incoming value + pre-gain-offset) * gain (update towards item)
45  * - (incoming value / gain) - pre-gain-offset (command from item)
46  *
47  * Gain can also specify unit of the result, converting otherwise bare numbers to ones with quantity.
48  *
49  *
50  * @author Sami Salonen - Initial contribution
51  */
52 @NonNullByDefault
53 public class ModbusGainOffsetProfile<Q extends Quantity<Q>> implements StateProfile {
54
55     private final Logger logger = LoggerFactory.getLogger(ModbusGainOffsetProfile.class);
56     private static final String PREGAIN_OFFSET_PARAM = "pre-gain-offset";
57     private static final String GAIN_PARAM = "gain";
58
59     private final ProfileCallback callback;
60     private final ProfileContext context;
61
62     private Optional<QuantityType<Dimensionless>> pregainOffset;
63     private Optional<QuantityType<Q>> gain;
64
65     public ModbusGainOffsetProfile(ProfileCallback callback, ProfileContext context) {
66         this.callback = callback;
67         this.context = context;
68         {
69             Object rawOffsetValue = orDefault("0", this.context.getConfiguration().get(PREGAIN_OFFSET_PARAM));
70             logger.debug("Configuring profile with {} parameter '{}'", PREGAIN_OFFSET_PARAM, rawOffsetValue);
71             pregainOffset = parameterAsQuantityType(PREGAIN_OFFSET_PARAM, rawOffsetValue, Units.ONE);
72
73         }
74         {
75             Object gainValue = orDefault("1", this.context.getConfiguration().get(GAIN_PARAM));
76             logger.debug("Configuring profile with {} parameter '{}'", GAIN_PARAM, gainValue);
77             gain = parameterAsQuantityType(GAIN_PARAM, gainValue);
78
79         }
80     }
81
82     public boolean isValid() {
83         return pregainOffset.isPresent() && gain.isPresent();
84     }
85
86     public Optional<QuantityType<Dimensionless>> getPregainOffset() {
87         return pregainOffset;
88     }
89
90     public Optional<QuantityType<Q>> getGain() {
91         return gain;
92     }
93
94     @Override
95     public ProfileTypeUID getProfileTypeUID() {
96         return ModbusProfiles.GAIN_OFFSET;
97     }
98
99     @Override
100     public void onStateUpdateFromItem(State state) {
101         // no-op
102     }
103
104     @Override
105     public void onCommandFromItem(Command command) {
106         Type result = applyGainOffset(command, false);
107         if (result instanceof Command) {
108             logger.trace("Command '{}' from item, sending converted '{}' state towards handler.", command, result);
109             callback.handleCommand((Command) result);
110         }
111     }
112
113     @Override
114     public void onCommandFromHandler(Command command) {
115         Type result = applyGainOffset(command, true);
116         if (result instanceof Command) {
117             logger.trace("Command '{}' from handler, sending converted '{}' command towards item.", command, result);
118             callback.sendCommand((Command) result);
119         }
120     }
121
122     @Override
123     public void onStateUpdateFromHandler(State state) {
124         State result = (State) applyGainOffset(state, true);
125         logger.trace("State update '{}' from handler, sending converted '{}' state towards item.", state, result);
126         callback.sendUpdate(result);
127     }
128
129     private Type applyGainOffset(Type state, boolean towardsItem) {
130         Type result = UnDefType.UNDEF;
131         Optional<QuantityType<Q>> localGain = gain;
132         Optional<QuantityType<Dimensionless>> localPregainOffset = pregainOffset;
133         if (localGain.isEmpty() || localPregainOffset.isEmpty()) {
134             logger.warn("Gain or pre-gain-offset unavailable. Check logs for configuration errors.");
135             return UnDefType.UNDEF;
136         } else if (state instanceof UnDefType) {
137             return UnDefType.UNDEF;
138         }
139
140         QuantityType<Q> gain = localGain.get();
141         QuantityType<Dimensionless> pregainOffsetQt = localPregainOffset.get();
142         String formula = towardsItem ? String.format("( '%s' + '%s') * '%s'", state, pregainOffsetQt, gain)
143                 : String.format("'%s'/'%s' - '%s'", state, gain, pregainOffsetQt);
144         if (state instanceof QuantityType) {
145             try {
146                 if (towardsItem) {
147                     @SuppressWarnings("unchecked") // xx.toUnit(ONE) returns null or QuantityType<Dimensionless>
148                     @Nullable
149                     QuantityType<Dimensionless> qtState = (QuantityType<Dimensionless>) (((QuantityType<?>) state)
150                             .toUnit(Units.ONE));
151                     if (qtState == null) {
152                         logger.warn("Profile can only process plain numbers from handler. Got unit {}. Returning UNDEF",
153                                 ((QuantityType<?>) state).getUnit());
154                         return UnDefType.UNDEF;
155                     }
156                     QuantityType<Dimensionless> offsetted = qtState.add(pregainOffsetQt);
157                     result = applyGainTowardsItem(offsetted, gain);
158                 } else {
159                     final QuantityType<?> qtState = (QuantityType<?>) state;
160                     result = applyGainTowardsHandler(qtState, gain).subtract(pregainOffsetQt);
161
162                 }
163             } catch (UnconvertibleException | UnsupportedOperationException e) {
164                 logger.warn(
165                         "Cannot apply gain ('{}') and pre-gain-offset ('{}') to state ('{}') (formula {}) because types do not match (towardsItem={}): {}",
166                         gain, pregainOffsetQt, state, formula, towardsItem, e.getMessage());
167                 return UnDefType.UNDEF;
168             }
169         } else if (state instanceof DecimalType) {
170             DecimalType decState = (DecimalType) state;
171             return applyGainOffset(new QuantityType<>(decState, Units.ONE), towardsItem);
172         } else if (state instanceof RefreshType) {
173             result = state;
174         } else {
175             logger.warn(
176                     "Gain '{}' cannot be applied to the incompatible state '{}' of type {} sent from the binding (towardsItem={}). Returning original state.",
177                     gain, state, state.getClass().getSimpleName(), towardsItem);
178             result = state;
179         }
180         return result;
181     }
182
183     private Optional<QuantityType<Q>> parameterAsQuantityType(String parameterName, Object parameterValue) {
184         return parameterAsQuantityType(parameterName, parameterValue, null);
185     }
186
187     private <QU extends Quantity<QU>> Optional<QuantityType<QU>> parameterAsQuantityType(String parameterName,
188             Object parameterValue, @Nullable Unit<QU> assertUnit) {
189         Optional<QuantityType<QU>> result = Optional.empty();
190         Unit<QU> sourceUnit = null;
191         if (parameterValue instanceof String) {
192             try {
193                 QuantityType<QU> qt = new QuantityType<>((String) parameterValue);
194                 result = Optional.of(qt);
195                 sourceUnit = qt.getUnit();
196             } catch (IllegalArgumentException e) {
197                 logger.error("Cannot convert value '{}' of parameter '{}' into a QuantityType.", parameterValue,
198                         parameterName);
199             }
200         } else if (parameterValue instanceof BigDecimal) {
201             BigDecimal parameterBigDecimal = (BigDecimal) parameterValue;
202             result = Optional.of(new QuantityType<QU>(parameterBigDecimal.toString()));
203         } else {
204             logger.error("Parameter '{}' is not of type String or BigDecimal", parameterName);
205             return result;
206         }
207         result = result.map(quantityType -> convertUnit(quantityType, assertUnit));
208         if (result.isEmpty()) {
209             logger.error("Unable to convert parameter '{}' to unit {}. Unit was {}.", parameterName, assertUnit,
210                     sourceUnit);
211         }
212         return result;
213     }
214
215     private <QU extends Quantity<QU>> @Nullable QuantityType<QU> convertUnit(QuantityType<QU> quantityType,
216             @Nullable Unit<QU> unit) {
217         if (unit == null) {
218             return quantityType;
219         }
220         QuantityType<QU> normalizedQt = quantityType.toUnit(unit);
221         if (normalizedQt != null) {
222             return normalizedQt;
223         } else {
224             return null;
225         }
226     }
227
228     /**
229      * Calculate qtState * gain or qtState/gain
230      *
231      * When the conversion is towards the handler (towardsItem=false), unit will be ONE
232      *
233      */
234     private <QU extends Quantity<QU>> QuantityType<QU> applyGainTowardsItem(QuantityType<Dimensionless> qtState,
235             QuantityType<QU> gainDelta) {
236         return new QuantityType<>(qtState.toBigDecimal().multiply(gainDelta.toBigDecimal()), gainDelta.getUnit());
237     }
238
239     private QuantityType<Dimensionless> applyGainTowardsHandler(QuantityType<?> qtState, QuantityType<?> gainDelta) {
240         QuantityType<?> plain = qtState.toUnit(gainDelta.getUnit());
241         if (plain == null) {
242             throw new UnconvertibleException(
243                     String.format("Cannot process command '%s', unit should compatible with gain", qtState));
244         }
245         return new QuantityType<>(plain.toBigDecimal().divide(gainDelta.toBigDecimal()), Units.ONE);
246     }
247
248     private static Object orDefault(Object defaultValue, @Nullable Object value) {
249         if (value == null) {
250             return defaultValue;
251         } else if (value instanceof String && ((String) value).isBlank()) {
252             return defaultValue;
253         } else {
254             return value;
255         }
256     }
257 }