[snmp] Apply UoM to number channels from an SNMP target (#10681)

* Add Unit handling functionality to SNMP Reads

Signed-off-by: James Melville <jamesmelville@gmail.com>

* Use core library for Unit parsing

Signed-off-by: James Melville <jamesmelville@gmail.com>
This commit is contained in:
James Melville 2021-06-20 19:30:37 +01:00 committed by GitHub
parent 22dd4aecd8
commit f73553347e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 69 additions and 5 deletions

View File

@ -94,6 +94,8 @@ For `string` channels the default `datatype` is `STRING` (i.e. the item's will b
If it is set to `IPADDRESS`, an SNMP IP address object is constructed from the item's value.
The `HEXSTRING` datatype converts a hexadecimal string (e.g. `aa bb 11`) to the respective octet string before sending data to the target (and vice versa for receiving data).
`number`-type channels can have a parameter `unit` if their `mode` is set to `READ`. This will result in a state update applying [UoM](https://www.openhab.org/docs/concepts/units-of-measurement.html) to the received data if the UoM symbol is recognised.
`switch`-type channels send a pre-defined value if they receive `ON` or `OFF` command in `WRITE` or `READ_WRITE` mode.
In `READ`, `READ_WRITE` or `TRAP` mode they change to either `ON` or `OFF` on these values.
The parameters used for defining the values are `onvalue` and `offvalue`.
@ -125,7 +127,7 @@ demo.things:
```
Thing snmp:target:router [ hostname="192.168.0.1", protocol="v2c" ] {
Channels:
Type number : inBytes [ oid=".1.3.6.1.2.1.31.1.1.1.6.2", mode="READ" ]
Type number : inBytes [ oid=".1.3.6.1.2.1.31.1.1.1.6.2", mode="READ", unit="B" ]
Type number : outBytes [ oid=".1.3.6.1.2.1.31.1.1.1.10.2", mode="READ" ]
Type number : if4Status [ oid="1.3.6.1.2.1.2.2.1.7.4", mode="TRAP" ]
Type switch : if4Command [ oid="1.3.6.1.2.1.2.2.1.7.4", mode="READ_WRITE", datatype="UINT32", onvalue="2", offvalue="0" ]
@ -138,6 +140,7 @@ demo.items:
```
Number inBytes "Router bytes in [%d]" { channel="snmp:target:router:inBytes" }
Number inGigaBytes "Router gigabytes in [%d GB]" { channel="snmp:target:router:inBytes" }
Number outBytes "Router bytes out [%d]" { channel="snmp:target:router:outBytes" }
Number if4Status "Router interface 4 status [%d]" { channel="snmp:target:router:if4Status" }
Switch if4Command "Router interface 4 switch [%s]" { channel="snmp:target:router:if4Command" }

View File

@ -15,6 +15,7 @@ package org.openhab.binding.snmp.internal;
import static org.openhab.binding.snmp.internal.SnmpBindingConstants.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
@ -26,6 +27,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.measure.Unit;
import javax.measure.format.MeasurementParseException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.config.SnmpChannelConfiguration;
@ -33,6 +37,7 @@ import org.openhab.binding.snmp.internal.config.SnmpInternalChannelConfiguration
import org.openhab.binding.snmp.internal.config.SnmpTargetConfiguration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
@ -45,6 +50,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.types.util.UnitUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.AbstractTarget;
@ -257,6 +263,7 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
Variable onValue = null;
Variable offValue = null;
State exceptionValue = UnDefType.UNDEF;
Unit<?> unit = null;
if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
if (datatype == null) {
@ -268,6 +275,17 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
if (configExceptionValue != null) {
exceptionValue = DecimalType.valueOf(configExceptionValue);
}
if (config.unit != null) {
if (config.mode != SnmpChannelMode.READ) {
logger.warn("units only supported for readonly channels, ignored for channel {}", channel.getUID());
} else {
try {
unit = UnitUtils.parseUnit(config.unit);
} catch (MeasurementParseException e) {
logger.warn("unrecognised unit '{}', ignored for channel '{}'", config.unit, channel.getUID());
}
}
}
} else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
if (datatype == null) {
datatype = SnmpDatatype.STRING;
@ -305,7 +323,7 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
return null;
}
return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(oid), config.mode, datatype, onValue,
offValue, exceptionValue, config.doNotLogException);
offValue, exceptionValue, config.doNotLogException, unit);
}
private void generateChannelConfigs() {
@ -341,10 +359,17 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
state = channelConfig.exceptionValue;
} else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
try {
BigDecimal numericState;
final @Nullable Unit<?> unit = channelConfig.unit;
if (channelConfig.datatype == SnmpDatatype.FLOAT) {
state = new DecimalType(value.toString());
numericState = new BigDecimal(value.toString());
} else {
state = new DecimalType(value.toLong());
numericState = BigDecimal.valueOf(value.toLong());
}
if (unit != null) {
state = new QuantityType<>(numericState, unit);
} else {
state = new DecimalType(numericState);
}
} catch (UnsupportedOperationException e) {
logger.warn("could not convert {} to number for channel {}", value, channelUID);

View File

@ -33,4 +33,6 @@ public class SnmpChannelConfiguration {
public @Nullable String exceptionValue;
public boolean doNotLogException = false;
public @Nullable String unit;
}

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.snmp.internal.config;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.SnmpChannelMode;
@ -38,9 +40,11 @@ public class SnmpInternalChannelConfiguration {
public final @Nullable Variable offValue;
public final State exceptionValue;
public final boolean doNotLogException;
public final @Nullable Unit<?> unit;
public SnmpInternalChannelConfiguration(ChannelUID channelUID, OID oid, SnmpChannelMode mode, SnmpDatatype datatype,
@Nullable Variable onValue, @Nullable Variable offValue, State exceptionValue, boolean doNotLogException) {
@Nullable Variable onValue, @Nullable Variable offValue, State exceptionValue, boolean doNotLogException,
@Nullable Unit<?> unit) {
this.channelUID = channelUID;
this.oid = oid;
this.mode = mode;
@ -49,5 +53,6 @@ public class SnmpInternalChannelConfiguration {
this.offValue = offValue;
this.exceptionValue = exceptionValue;
this.doNotLogException = doNotLogException;
this.unit = unit;
}
}

View File

@ -99,6 +99,12 @@
<description>Value to send if an SNMP exception occurs (default: UNDEF)</description>
<advanced>true</advanced>
</parameter>
<parameter name="unit" type="text">
<label>Unit Of Measurement</label>
<description>Unit of measurement (optional). The unit is used for representing the value in the GUI as well as for
converting incoming values (like from '°F' to '°C'). Examples: "°C", "°F"</description>
<advanced>true</advanced>
</parameter>
</config-description>
</channel-type>

View File

@ -170,6 +170,11 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype,
String onValue, String offValue, String exceptionValue) {
setup(channelTypeUID, channelMode, datatype, onValue, offValue, exceptionValue, null);
}
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype,
String onValue, String offValue, String exceptionValue, String unit) {
Map<String, Object> channelConfig = new HashMap<>();
Map<String, Object> thingConfig = new HashMap<>();
mocks = MockitoAnnotations.openMocks(this);
@ -195,6 +200,9 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
if (exceptionValue != null) {
channelConfig.put("exceptionValue", exceptionValue);
}
if (unit != null) {
channelConfig.put("unit", unit);
}
Channel channel = ChannelBuilder.create(CHANNEL_UID, itemType).withType(channelTypeUID)
.withConfiguration(new Configuration(channelConfig)).build();
thingBuilder.withChannel(channel);

View File

@ -22,7 +22,9 @@ import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ThingStatus;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
@ -109,6 +111,19 @@ public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest {
verifyStatus(ThingStatus.ONLINE);
}
@Test
public void testNumberChannelsProperlyHandlingUnits() throws IOException {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.FLOAT, null, null, null,
"°C");
PDU responsePDU = new PDU(PDU.RESPONSE,
Collections.singletonList(new VariableBinding(new OID(TEST_OID), new OctetString("12.4"))));
ResponseEvent event = new ResponseEvent("test", null, null, responsePDU, null);
thingHandler.onResponse(event);
verify(thingHandlerCallback, atLeast(1)).stateUpdated(eq(CHANNEL_UID),
eq(new QuantityType<>(12.4, SIUnits.CELSIUS)));
verifyStatus(ThingStatus.ONLINE);
}
@Test
public void testCancelingAsyncRequest() {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.FLOAT);