]> git.basschouten.com Git - openhab-addons.git/blob
88e30604725469f772ebc4938ca747c980e93e3b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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 cmd) {
108             logger.trace("Command '{}' from item, sending converted '{}' state towards handler.", command, result);
109             callback.handleCommand(cmd);
110         }
111     }
112
113     @Override
114     public void onCommandFromHandler(Command command) {
115         Type result = applyGainOffset(command, true);
116         if (result instanceof Command cmd) {
117             logger.trace("Command '{}' from handler, sending converted '{}' command towards item.", command, result);
118             callback.sendCommand(cmd);
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 quantityState) {
145             try {
146                 if (towardsItem) {
147                     @SuppressWarnings("unchecked") // xx.toUnit(ONE) returns null or QuantityType<Dimensionless>
148                     @Nullable
149                     QuantityType<Dimensionless> qtState = (QuantityType<Dimensionless>) (quantityState
150                             .toUnit(Units.ONE));
151                     if (qtState == null) {
152                         logger.warn("Profile can only process plain numbers from handler. Got unit {}. Returning UNDEF",
153                                 quantityState.getUnit());
154                         return UnDefType.UNDEF;
155                     }
156                     QuantityType<Dimensionless> offsetted = qtState.add(pregainOffsetQt);
157                     result = applyGainTowardsItem(offsetted, gain);
158                 } else {
159                     result = applyGainTowardsHandler(quantityState, gain).subtract(pregainOffsetQt);
160
161                 }
162             } catch (UnconvertibleException | UnsupportedOperationException e) {
163                 logger.warn(
164                         "Cannot apply gain ('{}') and pre-gain-offset ('{}') to state ('{}') (formula {}) because types do not match (towardsItem={}): {}",
165                         gain, pregainOffsetQt, state, formula, towardsItem, e.getMessage());
166                 return UnDefType.UNDEF;
167             }
168         } else if (state instanceof DecimalType decState) {
169             return applyGainOffset(new QuantityType<>(decState, Units.ONE), towardsItem);
170         } else if (state instanceof RefreshType) {
171             result = state;
172         } else {
173             logger.warn(
174                     "Gain '{}' cannot be applied to the incompatible state '{}' of type {} sent from the binding (towardsItem={}). Returning original state.",
175                     gain, state, state.getClass().getSimpleName(), towardsItem);
176             result = state;
177         }
178         return result;
179     }
180
181     private Optional<QuantityType<Q>> parameterAsQuantityType(String parameterName, Object parameterValue) {
182         return parameterAsQuantityType(parameterName, parameterValue, null);
183     }
184
185     private <QU extends Quantity<QU>> Optional<QuantityType<QU>> parameterAsQuantityType(String parameterName,
186             Object parameterValue, @Nullable Unit<QU> assertUnit) {
187         Optional<QuantityType<QU>> result = Optional.empty();
188         Unit<QU> sourceUnit = null;
189         if (parameterValue instanceof String str) {
190             try {
191                 QuantityType<QU> qt = new QuantityType<>(str);
192                 result = Optional.of(qt);
193                 sourceUnit = qt.getUnit();
194             } catch (IllegalArgumentException e) {
195                 logger.error("Cannot convert value '{}' of parameter '{}' into a QuantityType.", parameterValue,
196                         parameterName);
197             }
198         } else if (parameterValue instanceof BigDecimal parameterBigDecimal) {
199             result = Optional.of(new QuantityType<>(parameterBigDecimal.toString()));
200         } else {
201             logger.error("Parameter '{}' is not of type String or BigDecimal", parameterName);
202             return result;
203         }
204         result = result.map(quantityType -> convertUnit(quantityType, assertUnit));
205         if (result.isEmpty()) {
206             logger.error("Unable to convert parameter '{}' to unit {}. Unit was {}.", parameterName, assertUnit,
207                     sourceUnit);
208         }
209         return result;
210     }
211
212     private <QU extends Quantity<QU>> @Nullable QuantityType<QU> convertUnit(QuantityType<QU> quantityType,
213             @Nullable Unit<QU> unit) {
214         if (unit == null) {
215             return quantityType;
216         }
217         QuantityType<QU> normalizedQt = quantityType.toUnit(unit);
218         if (normalizedQt != null) {
219             return normalizedQt;
220         } else {
221             return null;
222         }
223     }
224
225     /**
226      * Calculate qtState * gain or qtState/gain
227      *
228      * When the conversion is towards the handler (towardsItem=false), unit will be ONE
229      *
230      */
231     private <QU extends Quantity<QU>> QuantityType<QU> applyGainTowardsItem(QuantityType<Dimensionless> qtState,
232             QuantityType<QU> gainDelta) {
233         return new QuantityType<>(qtState.toBigDecimal().multiply(gainDelta.toBigDecimal()), gainDelta.getUnit());
234     }
235
236     private QuantityType<Dimensionless> applyGainTowardsHandler(QuantityType<?> qtState, QuantityType<?> gainDelta) {
237         QuantityType<?> plain = qtState.toUnit(gainDelta.getUnit());
238         if (plain == null) {
239             throw new UnconvertibleException(
240                     String.format("Cannot process command '%s', unit should compatible with gain", qtState));
241         }
242         return new QuantityType<>(plain.toBigDecimal().divide(gainDelta.toBigDecimal()), Units.ONE);
243     }
244
245     private static Object orDefault(Object defaultValue, @Nullable Object value) {
246         if (value == null) {
247             return defaultValue;
248         } else if (value instanceof String str && str.isBlank()) {
249             return defaultValue;
250         } else {
251             return value;
252         }
253     }
254 }