[modbus] Gain-offset profile (QuantityType support) and writing of individual bits of holding registers (#9980)

* [modbus] gainOffset and bitMask profiles for working with modbus data

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README trailing whitespaces

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README and some final renaming

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] log error with incompatible units

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] gainOffset profile: test for incompatible unit

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] example renamed

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove unused fields

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] gainOffset profile: make configuration parameters optional

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] xml indentantion fix

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] static code analysis fixes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Minor fixes for null checking

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] remove comment

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] bit profile README disclaimer with many commands

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Grammar fixes in README

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Fix bit profile UI configuration

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Bit profile: Added possibility to invert value on read/write

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] fix typo with explanation of inverted

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] bit profile: unit tests for inverted parameter

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] spotless:apply

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] static checker fixes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] write bit feature in data thing

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* wip

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] resolve itest

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] fixes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove bit profile

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Fix data thing readStart validation

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] readme fix

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove bit profile test

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Invalidate REFRESH data cache with cacheful writes

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] cleanup

- abort if command is not convertible to 0/1 (previously wrote the
  cached data)
- fail fast conditionals instead of deep if's

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README Fix typo in example

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] fix data thing write when child of endpoint

Also added regression test

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* Update bundles/org.openhab.binding.modbus/src/main/resources/OH-INF/config/gainOffset.xml

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* [modbus] performance-optimized logging

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] README: Removing xtend syntax hint, not needed anymore

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] generics typing added

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] dead code

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] avoid supressing generic type warnings

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] unnecessary generics

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] rename type parameter name

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] QU (short for quantity output) generic type instead of Q2

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] Remove unused localization

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] profile constant visibility harmonized

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

* [modbus] spotless:apply

Signed-off-by: Sami Salonen <ssalonen@gmail.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Sami Salonen
2021-04-16 23:59:55 +03:00
committed by GitHub
parent b42101addc
commit 265fd30ba1
11 changed files with 1217 additions and 94 deletions

View File

@@ -15,6 +15,7 @@ package org.openhab.binding.modbus.handler;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -31,6 +32,7 @@ import org.openhab.core.io.transport.modbus.ModbusFailureCallback;
import org.openhab.core.io.transport.modbus.ModbusReadCallback;
import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.io.transport.modbus.PollTask;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@@ -96,6 +98,10 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
@Override
public synchronized void handle(AsyncModbusReadResult result) {
// Casting to allow registers.orElse(null) below..
Optional<@Nullable ModbusRegisterArray> registers = (Optional<@Nullable ModbusRegisterArray>) result
.getRegisters();
lastPolledDataCache.set(registers.orElse(null));
handleResult(new PollResult(result));
}
@@ -186,6 +192,7 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
private volatile @Nullable ModbusReadRequestBlueprint request;
private volatile boolean disposed;
private volatile List<ModbusDataThingHandler> childCallbacks = new CopyOnWriteArrayList<>();
private volatile AtomicReference<@Nullable ModbusRegisterArray> lastPolledDataCache = new AtomicReference<>();
private @NonNullByDefault({}) ModbusCommunicationInterface comms;
private ReadCallbackDelegator callbackDelegator = new ReadCallbackDelegator();
@@ -288,6 +295,7 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
unregisterPollTask();
this.callbackDelegator.resetCache();
comms = null;
lastPolledDataCache.set(null);
}
/**
@@ -420,6 +428,20 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
if (localRequest == null) {
return;
}
ModbusRegisterArray possiblyMutatedCache = lastPolledDataCache.get();
AtomicStampedValue<PollResult> lastPollResult = callbackDelegator.lastResult;
if (lastPollResult != null && possiblyMutatedCache != null) {
AsyncModbusReadResult lastSuccessfulPollResult = lastPollResult.getValue().result;
if (lastSuccessfulPollResult != null) {
ModbusRegisterArray lastRegisters = ((Optional<@Nullable ModbusRegisterArray>) lastSuccessfulPollResult
.getRegisters()).orElse(null);
if (lastRegisters != null && !possiblyMutatedCache.equals(lastRegisters)) {
// Register has been mutated in between by a data thing that writes "individual bits"
// Invalidate cache for a fresh poll
callbackDelegator.resetCache();
}
}
}
long oldDataThreshold = System.currentTimeMillis() - cacheMillis;
boolean cacheWasRecentEnoughForUpdate = cacheMillis > 0
@@ -438,4 +460,8 @@ public class ModbusPollerThingHandler extends BaseBridgeHandler {
}
}
}
public AtomicReference<@Nullable ModbusRegisterArray> getLastPolledDataCache() {
return lastPolledDataCache;
}
}

View File

@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -78,6 +79,7 @@ import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.HexUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
@@ -133,7 +135,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
private volatile @Nullable CascadedValueTransformationImpl writeTransformation;
private volatile Optional<Integer> readIndex = Optional.empty();
private volatile Optional<Integer> readSubIndex = Optional.empty();
private volatile @Nullable Integer writeStart;
private volatile Optional<Integer> writeStart = Optional.empty();
private volatile Optional<Integer> writeSubIndex = Optional.empty();
private volatile int pollStart;
private volatile int slaveId;
private volatile @Nullable ModbusReadFunctionCode functionCode;
@@ -200,8 +203,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
// We did not have JSON output from the transformation, so writeStart is absolute required. Abort if it is
// missing
Integer writeStart = this.writeStart;
if (writeStart == null) {
Optional<Integer> writeStart = this.writeStart;
if (writeStart.isEmpty()) {
logger.debug(
"Thing {} '{}': not processing command {} since writeStart is missing and transformation output is not a JSON",
getThing().getUID(), getThing().getLabel(), command);
@@ -216,7 +219,7 @@ public class ModbusDataThingHandler extends BaseThingHandler {
}
ModbusWriteRequestBlueprint request = requestFromCommand(channelUID, command, config, transformedCommand.get(),
writeStart);
writeStart.get());
if (request == null) {
return;
}
@@ -267,7 +270,9 @@ public class ModbusDataThingHandler extends BaseThingHandler {
ModbusWriteRequestBlueprint request;
boolean writeMultiple = config.isWriteMultipleEvenWithSingleRegisterOrCoil();
String writeType = config.getWriteType();
ModbusPollerThingHandler pollerHandler = this.pollerHandler;
if (writeType == null) {
// disposed thing
return null;
}
if (writeType.equals(WRITE_TYPE_COIL)) {
@@ -289,7 +294,44 @@ public class ModbusDataThingHandler extends BaseThingHandler {
logger.warn("Received command but write value type not set! Ignoring command");
return null;
}
ModbusRegisterArray data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
final ModbusRegisterArray data;
if (writeValueType.equals(ValueType.BIT)) {
if (writeSubIndex.isEmpty()) {
// Should not happen! should be in configuration error
logger.error("Bug: sub index not present but writeValueType=BIT. Should be in configuration error");
return null;
}
Optional<Boolean> commandBool = ModbusBitUtilities.translateCommand2Boolean(transformedCommand);
if (commandBool.isEmpty()) {
logger.warn(
"Data thing is configured to write individual bit but we received command that is not convertible to 0/1 bit. Ignoring.");
return null;
} else if (pollerHandler == null) {
logger.warn("Bug: sub index present but not child of poller. Should be in configuration erro");
return null;
}
// writing bit of an individual register. Using cache from poller
AtomicReference<@Nullable ModbusRegisterArray> cachedRegistersRef = pollerHandler
.getLastPolledDataCache();
ModbusRegisterArray mutatedRegisters = cachedRegistersRef
.updateAndGet(cachedRegisters -> cachedRegisters == null ? null
: combineCommandWithRegisters(cachedRegisters, writeStart, writeSubIndex.get(),
commandBool.get()));
if (mutatedRegisters == null) {
logger.warn(
"Received command to thing with writeValueType=bit (pointing to individual bit of a holding register) but internal cache not yet populated. Ignoring command");
return null;
}
// extract register (first byte index = register index * 2)
byte[] allMutatedBytes = mutatedRegisters.getBytes();
int writeStartRelative = writeStart - pollStart;
data = new ModbusRegisterArray(allMutatedBytes[writeStartRelative * 2],
allMutatedBytes[writeStartRelative * 2 + 1]);
} else {
data = ModbusBitUtilities.commandToRegisters(transformedCommand, writeValueType);
}
writeMultiple = writeMultiple || data.size() > 1;
request = new ModbusWriteRegisterRequestBlueprint(slaveId, writeStart, data, writeMultiple,
config.getWriteMaxTries());
@@ -304,6 +346,33 @@ public class ModbusDataThingHandler extends BaseThingHandler {
return request;
}
/**
* Combine boolean-like command with registers. Updated registers are returned
*
* @return
*/
private ModbusRegisterArray combineCommandWithRegisters(ModbusRegisterArray registers, int registerIndex,
int bitIndex, boolean b) {
byte[] allBytes = registers.getBytes();
int bitIndexWithinRegister = bitIndex % 16;
boolean hiByte = bitIndexWithinRegister >= 8;
int indexWithinByte = bitIndexWithinRegister % 8;
int registerIndexRelative = registerIndex - pollStart;
int byteIndex = 2 * registerIndexRelative + (hiByte ? 0 : 1);
if (b) {
allBytes[byteIndex] |= 1 << indexWithinByte;
} else {
allBytes[byteIndex] &= ~(1 << indexWithinByte);
}
if (logger.isTraceEnabled()) {
logger.trace(
"Boolean-like command {} from item, combining command with internal register ({}) with registerIndex={} (relative {}), bitIndex={}, resulting register {}",
b, HexUtils.bytesToHex(registers.getBytes()), registerIndex, registerIndexRelative, bitIndex,
HexUtils.bytesToHex(allBytes));
}
return new ModbusRegisterArray(allBytes);
}
private void processJsonTransform(Command command, String transformOutput) {
ModbusCommunicationInterface localComms = this.comms;
if (localComms == null) {
@@ -398,7 +467,8 @@ public class ModbusDataThingHandler extends BaseThingHandler {
writeTransformation = null;
readIndex = Optional.empty();
readSubIndex = Optional.empty();
writeStart = null;
writeStart = Optional.empty();
writeSubIndex = Optional.empty();
pollStart = 0;
slaveId = 0;
comms = null;
@@ -553,19 +623,6 @@ public class ModbusDataThingHandler extends BaseThingHandler {
}
}
if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) {
String errmsg = String.format(
"Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s",
ModbusConstants.ValueType.BIT, config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
} else if (!writingCoil && localWriteValueType.getBits() < 16) {
// trying to write holding registers with < 16 bit value types. Not supported
String errmsg = String.format(
"Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s",
config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
}
try {
if (!writeParametersHavingTransformationOnly) {
String localWriteStart = config.getWriteStart();
@@ -574,13 +631,57 @@ public class ModbusDataThingHandler extends BaseThingHandler {
config.getWriteStart());
throw new ModbusConfigurationException(errmsg);
}
writeStart = Integer.parseInt(localWriteStart.trim());
String[] writeParts = localWriteStart.split("\\.", 2);
try {
writeStart = Optional.of(Integer.parseInt(writeParts[0]));
if (writeParts.length == 2) {
writeSubIndex = Optional.of(Integer.parseInt(writeParts[1]));
} else {
writeSubIndex = Optional.empty();
}
} catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
config.getReadStart());
throw new ModbusConfigurationException(errmsg);
}
}
} catch (IllegalArgumentException e) {
String errmsg = String.format("Thing %s invalid writeStart: %s", getThing().getUID(),
config.getWriteStart());
throw new ModbusConfigurationException(errmsg);
}
if (writingCoil && !ModbusConstants.ValueType.BIT.equals(localWriteValueType)) {
String errmsg = String.format(
"Invalid writeValueType: Only writeValueType='%s' (or undefined) supported with coils. Value type was: %s",
ModbusConstants.ValueType.BIT, config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
} else if (writeSubIndex.isEmpty() && !writingCoil && localWriteValueType.getBits() < 16) {
// trying to write holding registers with < 16 bit value types. Not supported
String errmsg = String.format(
"Invalid writeValueType: Only writeValueType with larger or equal to 16 bits are supported holding registers. Value type was: %s",
config.getWriteValueType());
throw new ModbusConfigurationException(errmsg);
}
if (writeSubIndex.isPresent()) {
if (writeValueTypeMissing || writeTypeMissing || !WRITE_TYPE_HOLDING.equals(config.getWriteType())
|| !ModbusConstants.ValueType.BIT.equals(localWriteValueType) || childOfEndpoint) {
String errmsg = String.format(
"Thing %s invalid writeType, writeValueType or parent. Since writeStart=X.Y, one should set writeType=holding, writeValueType=bit and have the thing as child of poller",
getThing().getUID(), config.getWriteStart());
throw new ModbusConfigurationException(errmsg);
}
ModbusReadRequestBlueprint readRequest = this.readRequest;
if (readRequest == null
|| readRequest.getFunctionCode() != ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS) {
String errmsg = String.format(
"Thing %s invalid. Since writeStart=X.Y, expecting poller reading holding registers.",
getThing().getUID());
throw new ModbusConfigurationException(errmsg);
}
}
validateWriteIndex();
} else {
isWriteEnabled = false;
}
@@ -646,6 +747,41 @@ public class ModbusDataThingHandler extends BaseThingHandler {
}
}
private void validateWriteIndex() throws ModbusConfigurationException {
@Nullable
ModbusReadRequestBlueprint readRequest = this.readRequest;
if (!writeStart.isPresent() || !writeSubIndex.isPresent()) {
//
// this validation is really about writeStart=X.Y validation
//
return;
} else if (readRequest == null) {
// should not happen, already validated
throw new ModbusConfigurationException("Must poll data with writeStart=X.Y");
}
if (writeSubIndex.isPresent() && (writeSubIndex.get() + 1) > 16) {
// the sub index Y (in X.Y) is above the register limits
String errmsg = String.format("readStart=X.Y, the value Y is too large");
throw new ModbusConfigurationException(errmsg);
}
// Determine bit positions polled, both start and end inclusive
int pollStartBitIndex = readRequest.getReference() * 16;
int pollEndBitIndex = pollStartBitIndex + readRequest.getDataLength() * 16 - 1;
// Determine bit positions read, both start and end inclusive
int writeStartBitIndex = writeStart.get() * 16 + readSubIndex.orElse(0);
int writeEndBitIndex = writeStartBitIndex - 1;
if (writeStartBitIndex < pollStartBitIndex || writeEndBitIndex > pollEndBitIndex) {
String errmsg = String.format(
"Out-of-bounds: Poller is reading from index %d to %d (inclusive) but this thing configured to write starting from element %d. Must write within polled limits",
pollStartBitIndex / 16, pollEndBitIndex / 16, writeStart.get());
throw new ModbusConfigurationException(errmsg);
}
}
private boolean containsOnOff(List<Class<? extends State>> channelAcceptedDataTypes) {
return channelAcceptedDataTypes.stream().anyMatch(clz -> {
return clz.equals(OnOffType.class);

View File

@@ -0,0 +1,258 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.profiles;
import java.math.BigDecimal;
import java.util.Optional;
import javax.measure.Quantity;
import javax.measure.UnconvertibleException;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfile;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Profile for applying gain and offset to values.
*
* Output of the profile is
* - (incoming value + pre-gain-offset) * gain (update towards item)
* - (incoming value / gain) - pre-gain-offset (command from item)
*
* Gain can also specify unit of the result, converting otherwise bare numbers to ones with quantity.
*
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class ModbusGainOffsetProfile<Q extends Quantity<Q>> implements StateProfile {
private final Logger logger = LoggerFactory.getLogger(ModbusGainOffsetProfile.class);
private static final String PREGAIN_OFFSET_PARAM = "pre-gain-offset";
private static final String GAIN_PARAM = "gain";
private final ProfileCallback callback;
private final ProfileContext context;
private Optional<QuantityType<Dimensionless>> pregainOffset;
private Optional<QuantityType<Q>> gain;
public ModbusGainOffsetProfile(ProfileCallback callback, ProfileContext context) {
this.callback = callback;
this.context = context;
{
Object rawOffsetValue = orDefault("0", this.context.getConfiguration().get(PREGAIN_OFFSET_PARAM));
logger.debug("Configuring profile with {} parameter '{}'", PREGAIN_OFFSET_PARAM, rawOffsetValue);
pregainOffset = parameterAsQuantityType(PREGAIN_OFFSET_PARAM, rawOffsetValue, Units.ONE);
}
{
Object gainValue = orDefault("1", this.context.getConfiguration().get(GAIN_PARAM));
logger.debug("Configuring profile with {} parameter '{}'", GAIN_PARAM, gainValue);
gain = parameterAsQuantityType(GAIN_PARAM, gainValue);
}
}
public boolean isValid() {
return pregainOffset.isPresent() && gain.isPresent();
}
public Optional<QuantityType<Dimensionless>> getPregainOffset() {
return pregainOffset;
}
public Optional<QuantityType<Q>> getGain() {
return gain;
}
@Override
public ProfileTypeUID getProfileTypeUID() {
return ModbusProfiles.GAIN_OFFSET;
}
@Override
public void onStateUpdateFromItem(State state) {
// no-op
}
@Override
public void onCommandFromItem(Command command) {
Type result = applyGainOffset(command, false);
if (result instanceof Command) {
logger.trace("Command '{}' from item, sending converted '{}' state towards handler.", command, result);
callback.handleCommand((Command) result);
}
}
@Override
public void onCommandFromHandler(Command command) {
Type result = applyGainOffset(command, true);
if (result instanceof Command) {
logger.trace("Command '{}' from handler, sending converted '{}' command towards item.", command, result);
callback.sendCommand((Command) result);
}
}
@Override
public void onStateUpdateFromHandler(State state) {
State result = (State) applyGainOffset(state, true);
logger.trace("State update '{}' from handler, sending converted '{}' state towards item.", state, result);
callback.sendUpdate(result);
}
private Type applyGainOffset(Type state, boolean towardsItem) {
Type result = UnDefType.UNDEF;
Optional<QuantityType<Q>> localGain = gain;
Optional<QuantityType<Dimensionless>> localPregainOffset = pregainOffset;
if (localGain.isEmpty() || localPregainOffset.isEmpty()) {
logger.warn("Gain or offset unavailable. Check logs for configuration errors.");
return UnDefType.UNDEF;
} else if (state instanceof UnDefType) {
return UnDefType.UNDEF;
}
QuantityType<Q> gain = localGain.get();
QuantityType<Dimensionless> pregainOffsetQt = localPregainOffset.get();
String formula = towardsItem ? String.format("( '%s' + '%s') * '%s'", state, pregainOffsetQt, gain)
: String.format("'%s'/'%s' - '%s'", state, gain, pregainOffsetQt);
if (state instanceof QuantityType) {
try {
if (towardsItem) {
@SuppressWarnings("unchecked") // xx.toUnit(ONE) returns null or QuantityType<Dimensionless>
@Nullable
QuantityType<Dimensionless> qtState = (QuantityType<Dimensionless>) (((QuantityType<?>) state)
.toUnit(Units.ONE));
if (qtState == null) {
logger.warn("Profile can only process plain numbers from handler. Got unit {}. Returning UNDEF",
((QuantityType<?>) state).getUnit());
return UnDefType.UNDEF;
}
QuantityType<Dimensionless> offsetted = qtState.add(pregainOffsetQt);
result = applyGainTowardsItem(offsetted, gain);
} else {
final QuantityType<?> qtState = (QuantityType<?>) state;
result = applyGainTowardsHandler(qtState, gain).subtract(pregainOffsetQt);
}
} catch (UnconvertibleException | UnsupportedOperationException e) {
logger.warn(
"Cannot apply gain ('{}') and offset ('{}') to state ('{}') (formula {}) because types do not match (towardsItem={}): {}",
gain, pregainOffsetQt, state, formula, towardsItem, e.getMessage());
return UnDefType.UNDEF;
}
} else if (state instanceof DecimalType) {
DecimalType decState = (DecimalType) state;
return applyGainOffset(new QuantityType<>(decState, Units.ONE), towardsItem);
} else if (state instanceof RefreshType) {
result = state;
} else {
logger.warn(
"Gain '{}' cannot be applied to the incompatible state '{}' of type {} sent from the binding (towardsItem={}). Returning original state.",
gain, state, state.getClass().getSimpleName(), towardsItem);
result = state;
}
return result;
}
private Optional<QuantityType<Q>> parameterAsQuantityType(String parameterName, Object parameterValue) {
return parameterAsQuantityType(parameterName, parameterValue, null);
}
private <QU extends Quantity<QU>> Optional<QuantityType<QU>> parameterAsQuantityType(String parameterName,
Object parameterValue, @Nullable Unit<QU> assertUnit) {
Optional<QuantityType<QU>> result = Optional.empty();
Unit<QU> sourceUnit = null;
if (parameterValue instanceof String) {
try {
QuantityType<QU> qt = new QuantityType<>((String) parameterValue);
result = Optional.of(qt);
sourceUnit = qt.getUnit();
} catch (IllegalArgumentException e) {
logger.error("Cannot convert value '{}' of parameter '{}' into a QuantityType.", parameterValue,
parameterName);
}
} else if (parameterValue instanceof BigDecimal) {
BigDecimal parameterBigDecimal = (BigDecimal) parameterValue;
result = Optional.of(new QuantityType<QU>(parameterBigDecimal.toString()));
} else {
logger.error("Parameter '{}' is not of type String or BigDecimal", parameterName);
return result;
}
result = result.map(quantityType -> convertUnit(quantityType, assertUnit));
if (result.isEmpty()) {
logger.error("Unable to convert parameter '{}' to unit {}. Unit was {}.", parameterName, assertUnit,
sourceUnit);
}
return result;
}
private <QU extends Quantity<QU>> @Nullable QuantityType<QU> convertUnit(QuantityType<QU> quantityType,
@Nullable Unit<QU> unit) {
if (unit == null) {
return quantityType;
}
QuantityType<QU> normalizedQt = quantityType.toUnit(unit);
if (normalizedQt != null) {
return normalizedQt;
} else {
return null;
}
}
/**
* Calculate qtState * gain or qtState/gain
*
* When the conversion is towards the handler (towardsItem=false), unit will be ONE
*
*/
@SuppressWarnings("unchecked") // Safe cast since QU = Dimensionless * QU
private <QU extends Quantity<QU>> QuantityType<QU> applyGainTowardsItem(QuantityType<Dimensionless> qtState,
QuantityType<QU> gainDelta) {
return (QuantityType<QU>) qtState.multiply(gainDelta);
}
private QuantityType<Dimensionless> applyGainTowardsHandler(QuantityType<?> qtState, QuantityType<?> gainDelta) {
QuantityType<?> plain = qtState.toUnit(gainDelta.getUnit());
if (plain == null) {
throw new UnconvertibleException(
String.format("Cannot process command '%s', unit should compatible with gain", qtState));
}
return new QuantityType<>(plain.toBigDecimal().divide(gainDelta.toBigDecimal()), Units.ONE);
}
private static Object orDefault(Object defaultValue, @Nullable Object value) {
if (value == null) {
return defaultValue;
} else if (value instanceof String && ((String) value).isBlank()) {
return defaultValue;
} else {
return value;
}
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.profiles;
import static org.openhab.binding.modbus.internal.profiles.ModbusProfiles.*;
import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.profiles.Profile;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileFactory;
import org.openhab.core.thing.profiles.ProfileType;
import org.openhab.core.thing.profiles.ProfileTypeProvider;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.osgi.service.component.annotations.Component;
/**
* A factory and advisor for modbus profiles.
*
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
@Component(service = { ProfileFactory.class, ProfileTypeProvider.class })
public class ModbusProfileFactory implements ProfileFactory, ProfileTypeProvider {
private static final Set<ProfileType> SUPPORTED_PROFILE_TYPES = Set.of(GAIN_OFFSET_TYPE);
private static final Set<ProfileTypeUID> SUPPORTED_PROFILE_TYPE_UIDS = Set.of(GAIN_OFFSET);
@Override
public @Nullable Profile createProfile(ProfileTypeUID profileTypeUID, ProfileCallback callback,
ProfileContext context) {
if (GAIN_OFFSET.equals(profileTypeUID)) {
return new ModbusGainOffsetProfile<>(callback, context);
} else {
return null;
}
}
@Override
public Collection<ProfileType> getProfileTypes(@Nullable Locale locale) {
return SUPPORTED_PROFILE_TYPES;
}
@Override
public Collection<ProfileTypeUID> getSupportedProfileTypeUIDs() {
return SUPPORTED_PROFILE_TYPE_UIDS;
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.modbus.internal.profiles;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.profiles.ProfileTypeBuilder;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfileType;
/**
* Modbus profile constants.
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public interface ModbusProfiles {
static final String MODBUS_SCOPE = "modbus";
static final ProfileTypeUID GAIN_OFFSET = new ProfileTypeUID(MODBUS_SCOPE, "gainOffset");
static final StateProfileType GAIN_OFFSET_TYPE = ProfileTypeBuilder.newState(GAIN_OFFSET, "Gain-Offset Correction")
.build();
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="profile:modbus:gainOffset">
<parameter name="pre-gain-offset" type="decimal">
<label>Pre-gain Offset</label>
<description>Offset to add to raw value towards the item (before the gain). The negative
offset will be applied in the
reverse direction (before inverting the gain). If omitted, zero offset is used.</description>
</parameter>
<parameter name="gain" type="text">
<label>Gain</label>
<description>Gain to apply to the state towards the item. One can also specify the unit to declare resulting unit.
This is used as divisor for values in the reverse direction. If omitted, gain of 1 is used.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -26,7 +26,7 @@
</channels>
<config-description>
<!-- what to read -->
<parameter name="readStart" type="text" pattern="^(0|[1-9][0-9]*(\.[0-9]{1,2})?)?$">
<parameter name="readStart" type="text" pattern="^(0|[0-9][0-9]*(\.[0-9]{1,2})?)?$">
<label>Read Address</label>
<description><![CDATA[Start address to start reading the value. Use empty for write-only things.
<br />
@@ -81,7 +81,9 @@
<parameter name="writeStart" type="text">
<label>Write Address</label>
<description><![CDATA[Start address of the first holding register or coil in the write. Use empty for read-only things.
<br />Use zero based address, e.g. in place of 400001 (first holding register), use the address 0. This address is passed to data frame as is.]]></description>
<br />Use zero based address, e.g. in place of 400001 (first holding register), use the address 0. This address is passed to data frame as is.
<br />One can write individual bits of an register using X.Y format where X is the register and Y is the bit (0 refers to least significant bit).
]]></description>
</parameter>
<parameter name="writeType" type="text">
<label>Write Type</label>