2 * Copyright (c) 2010-2024 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.modbus.internal.profiles;
15 import java.math.BigDecimal;
16 import java.util.Optional;
18 import javax.measure.Quantity;
19 import javax.measure.UnconvertibleException;
20 import javax.measure.Unit;
21 import javax.measure.quantity.Dimensionless;
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;
41 * Profile for applying gain and offset to values.
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)
47 * Gain can also specify unit of the result, converting otherwise bare numbers to ones with quantity.
50 * @author Sami Salonen - Initial contribution
53 public class ModbusGainOffsetProfile<Q extends Quantity<Q>> implements StateProfile {
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";
59 private final ProfileCallback callback;
60 private final ProfileContext context;
62 private Optional<QuantityType<Dimensionless>> pregainOffset;
63 private Optional<QuantityType<Q>> gain;
65 public ModbusGainOffsetProfile(ProfileCallback callback, ProfileContext context) {
66 this.callback = callback;
67 this.context = context;
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);
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);
82 public boolean isValid() {
83 return pregainOffset.isPresent() && gain.isPresent();
86 public Optional<QuantityType<Dimensionless>> getPregainOffset() {
90 public Optional<QuantityType<Q>> getGain() {
95 public ProfileTypeUID getProfileTypeUID() {
96 return ModbusProfiles.GAIN_OFFSET;
100 public void onStateUpdateFromItem(State state) {
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);
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);
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);
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;
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) {
147 @SuppressWarnings("unchecked") // xx.toUnit(ONE) returns null or QuantityType<Dimensionless>
149 QuantityType<Dimensionless> qtState = (QuantityType<Dimensionless>) (quantityState
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;
156 QuantityType<Dimensionless> offsetted = qtState.add(pregainOffsetQt);
157 result = applyGainTowardsItem(offsetted, gain);
159 result = applyGainTowardsHandler(quantityState, gain).subtract(pregainOffsetQt);
162 } catch (UnconvertibleException | UnsupportedOperationException e) {
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;
168 } else if (state instanceof DecimalType decState) {
169 return applyGainOffset(new QuantityType<>(decState, Units.ONE), towardsItem);
170 } else if (state instanceof RefreshType) {
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);
181 private Optional<QuantityType<Q>> parameterAsQuantityType(String parameterName, Object parameterValue) {
182 return parameterAsQuantityType(parameterName, parameterValue, null);
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) {
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,
198 } else if (parameterValue instanceof BigDecimal parameterBigDecimal) {
199 result = Optional.of(new QuantityType<>(parameterBigDecimal.toString()));
201 logger.error("Parameter '{}' is not of type String or BigDecimal", parameterName);
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,
212 private <QU extends Quantity<QU>> @Nullable QuantityType<QU> convertUnit(QuantityType<QU> quantityType,
213 @Nullable Unit<QU> unit) {
217 QuantityType<QU> normalizedQt = quantityType.toUnit(unit);
218 if (normalizedQt != null) {
226 * Calculate qtState * gain or qtState/gain
228 * When the conversion is towards the handler (towardsItem=false), unit will be ONE
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());
236 private QuantityType<Dimensionless> applyGainTowardsHandler(QuantityType<?> qtState, QuantityType<?> gainDelta) {
237 QuantityType<?> plain = qtState.toUnit(gainDelta.getUnit());
239 throw new UnconvertibleException(
240 String.format("Cannot process command '%s', unit should compatible with gain", qtState));
242 return new QuantityType<>(plain.toBigDecimal().divide(gainDelta.toBigDecimal()), Units.ONE);
245 private static Object orDefault(Object defaultValue, @Nullable Object value) {
248 } else if (value instanceof String str && str.isBlank()) {