From f73553347e3ba622fd2c5a8e4da8867b8437e4c0 Mon Sep 17 00:00:00 2001 From: James Melville Date: Sun, 20 Jun 2021 19:30:37 +0100 Subject: [PATCH] [snmp] Apply UoM to number channels from an SNMP target (#10681) * Add Unit handling functionality to SNMP Reads Signed-off-by: James Melville * Use core library for Unit parsing Signed-off-by: James Melville --- bundles/org.openhab.binding.snmp/README.md | 5 ++- .../snmp/internal/SnmpTargetHandler.java | 31 +++++++++++++++++-- .../config/SnmpChannelConfiguration.java | 2 ++ .../SnmpInternalChannelConfiguration.java | 7 ++++- .../resources/OH-INF/thing/thing-types.xml | 6 ++++ .../AbstractSnmpTargetHandlerTest.java | 8 +++++ .../snmp/internal/SnmpTargetHandlerTest.java | 15 +++++++++ 7 files changed, 69 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.snmp/README.md b/bundles/org.openhab.binding.snmp/README.md index 89e74e1d0..951dcc472 100644 --- a/bundles/org.openhab.binding.snmp/README.md +++ b/bundles/org.openhab.binding.snmp/README.md @@ -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" } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpTargetHandler.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpTargetHandler.java index 1a121f72a..d1c97c73c 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpTargetHandler.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpTargetHandler.java @@ -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); diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpChannelConfiguration.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpChannelConfiguration.java index c3835bf1b..0bfa59e99 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpChannelConfiguration.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpChannelConfiguration.java @@ -33,4 +33,6 @@ public class SnmpChannelConfiguration { public @Nullable String exceptionValue; public boolean doNotLogException = false; + + public @Nullable String unit; } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpInternalChannelConfiguration.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpInternalChannelConfiguration.java index 37cc67695..f8cd7ee72 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpInternalChannelConfiguration.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpInternalChannelConfiguration.java @@ -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; } } diff --git a/bundles/org.openhab.binding.snmp/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.snmp/src/main/resources/OH-INF/thing/thing-types.xml index 700936dea..a34b75cb7 100644 --- a/bundles/org.openhab.binding.snmp/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.snmp/src/main/resources/OH-INF/thing/thing-types.xml @@ -99,6 +99,12 @@ Value to send if an SNMP exception occurs (default: UNDEF) true + + + 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" + true + diff --git a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/AbstractSnmpTargetHandlerTest.java b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/AbstractSnmpTargetHandlerTest.java index 85f2c231e..2500b4925 100644 --- a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/AbstractSnmpTargetHandlerTest.java +++ b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/AbstractSnmpTargetHandlerTest.java @@ -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 channelConfig = new HashMap<>(); Map 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); diff --git a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SnmpTargetHandlerTest.java b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SnmpTargetHandlerTest.java index b60c48357..05885b04e 100644 --- a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SnmpTargetHandlerTest.java +++ b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SnmpTargetHandlerTest.java @@ -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);