[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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user