[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:
parent
22dd4aecd8
commit
f73553347e
@ -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.
|
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).
|
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.
|
`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.
|
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`.
|
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" ] {
|
Thing snmp:target:router [ hostname="192.168.0.1", protocol="v2c" ] {
|
||||||
Channels:
|
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 : 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 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" ]
|
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 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 outBytes "Router bytes out [%d]" { channel="snmp:target:router:outBytes" }
|
||||||
Number if4Status "Router interface 4 status [%d]" { channel="snmp:target:router:if4Status" }
|
Number if4Status "Router interface 4 status [%d]" { channel="snmp:target:router:if4Status" }
|
||||||
Switch if4Command "Router interface 4 switch [%s]" { channel="snmp:target:router:if4Command" }
|
Switch if4Command "Router interface 4 switch [%s]" { channel="snmp:target:router:if4Command" }
|
||||||
|
|||||||
@ -15,6 +15,7 @@ package org.openhab.binding.snmp.internal;
|
|||||||
import static org.openhab.binding.snmp.internal.SnmpBindingConstants.*;
|
import static org.openhab.binding.snmp.internal.SnmpBindingConstants.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -26,6 +27,9 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
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.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.snmp.internal.config.SnmpChannelConfiguration;
|
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.binding.snmp.internal.config.SnmpTargetConfiguration;
|
||||||
import org.openhab.core.library.types.DecimalType;
|
import org.openhab.core.library.types.DecimalType;
|
||||||
import org.openhab.core.library.types.OnOffType;
|
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.types.StringType;
|
||||||
import org.openhab.core.thing.Channel;
|
import org.openhab.core.thing.Channel;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
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.RefreshType;
|
||||||
import org.openhab.core.types.State;
|
import org.openhab.core.types.State;
|
||||||
import org.openhab.core.types.UnDefType;
|
import org.openhab.core.types.UnDefType;
|
||||||
|
import org.openhab.core.types.util.UnitUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.snmp4j.AbstractTarget;
|
import org.snmp4j.AbstractTarget;
|
||||||
@ -257,6 +263,7 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
|
|||||||
Variable onValue = null;
|
Variable onValue = null;
|
||||||
Variable offValue = null;
|
Variable offValue = null;
|
||||||
State exceptionValue = UnDefType.UNDEF;
|
State exceptionValue = UnDefType.UNDEF;
|
||||||
|
Unit<?> unit = null;
|
||||||
|
|
||||||
if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
|
if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
|
||||||
if (datatype == null) {
|
if (datatype == null) {
|
||||||
@ -268,6 +275,17 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
|
|||||||
if (configExceptionValue != null) {
|
if (configExceptionValue != null) {
|
||||||
exceptionValue = DecimalType.valueOf(configExceptionValue);
|
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())) {
|
} else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
|
||||||
if (datatype == null) {
|
if (datatype == null) {
|
||||||
datatype = SnmpDatatype.STRING;
|
datatype = SnmpDatatype.STRING;
|
||||||
@ -305,7 +323,7 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(oid), config.mode, datatype, onValue,
|
return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(oid), config.mode, datatype, onValue,
|
||||||
offValue, exceptionValue, config.doNotLogException);
|
offValue, exceptionValue, config.doNotLogException, unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateChannelConfigs() {
|
private void generateChannelConfigs() {
|
||||||
@ -341,10 +359,17 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
|
|||||||
state = channelConfig.exceptionValue;
|
state = channelConfig.exceptionValue;
|
||||||
} else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
|
} else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
|
||||||
try {
|
try {
|
||||||
|
BigDecimal numericState;
|
||||||
|
final @Nullable Unit<?> unit = channelConfig.unit;
|
||||||
if (channelConfig.datatype == SnmpDatatype.FLOAT) {
|
if (channelConfig.datatype == SnmpDatatype.FLOAT) {
|
||||||
state = new DecimalType(value.toString());
|
numericState = new BigDecimal(value.toString());
|
||||||
} else {
|
} 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) {
|
} catch (UnsupportedOperationException e) {
|
||||||
logger.warn("could not convert {} to number for channel {}", value, channelUID);
|
logger.warn("could not convert {} to number for channel {}", value, channelUID);
|
||||||
|
|||||||
@ -33,4 +33,6 @@ public class SnmpChannelConfiguration {
|
|||||||
public @Nullable String exceptionValue;
|
public @Nullable String exceptionValue;
|
||||||
|
|
||||||
public boolean doNotLogException = false;
|
public boolean doNotLogException = false;
|
||||||
|
|
||||||
|
public @Nullable String unit;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.snmp.internal.config;
|
package org.openhab.binding.snmp.internal.config;
|
||||||
|
|
||||||
|
import javax.measure.Unit;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.snmp.internal.SnmpChannelMode;
|
import org.openhab.binding.snmp.internal.SnmpChannelMode;
|
||||||
@ -38,9 +40,11 @@ public class SnmpInternalChannelConfiguration {
|
|||||||
public final @Nullable Variable offValue;
|
public final @Nullable Variable offValue;
|
||||||
public final State exceptionValue;
|
public final State exceptionValue;
|
||||||
public final boolean doNotLogException;
|
public final boolean doNotLogException;
|
||||||
|
public final @Nullable Unit<?> unit;
|
||||||
|
|
||||||
public SnmpInternalChannelConfiguration(ChannelUID channelUID, OID oid, SnmpChannelMode mode, SnmpDatatype datatype,
|
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.channelUID = channelUID;
|
||||||
this.oid = oid;
|
this.oid = oid;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
@ -49,5 +53,6 @@ public class SnmpInternalChannelConfiguration {
|
|||||||
this.offValue = offValue;
|
this.offValue = offValue;
|
||||||
this.exceptionValue = exceptionValue;
|
this.exceptionValue = exceptionValue;
|
||||||
this.doNotLogException = doNotLogException;
|
this.doNotLogException = doNotLogException;
|
||||||
|
this.unit = unit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,12 @@
|
|||||||
<description>Value to send if an SNMP exception occurs (default: UNDEF)</description>
|
<description>Value to send if an SNMP exception occurs (default: UNDEF)</description>
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
</parameter>
|
</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>
|
</config-description>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
|
|||||||
@ -170,6 +170,11 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
|
|||||||
|
|
||||||
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype,
|
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype,
|
||||||
String onValue, String offValue, String exceptionValue) {
|
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> channelConfig = new HashMap<>();
|
||||||
Map<String, Object> thingConfig = new HashMap<>();
|
Map<String, Object> thingConfig = new HashMap<>();
|
||||||
mocks = MockitoAnnotations.openMocks(this);
|
mocks = MockitoAnnotations.openMocks(this);
|
||||||
@ -195,6 +200,9 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
|
|||||||
if (exceptionValue != null) {
|
if (exceptionValue != null) {
|
||||||
channelConfig.put("exceptionValue", exceptionValue);
|
channelConfig.put("exceptionValue", exceptionValue);
|
||||||
}
|
}
|
||||||
|
if (unit != null) {
|
||||||
|
channelConfig.put("unit", unit);
|
||||||
|
}
|
||||||
Channel channel = ChannelBuilder.create(CHANNEL_UID, itemType).withType(channelTypeUID)
|
Channel channel = ChannelBuilder.create(CHANNEL_UID, itemType).withType(channelTypeUID)
|
||||||
.withConfiguration(new Configuration(channelConfig)).build();
|
.withConfiguration(new Configuration(channelConfig)).build();
|
||||||
thingBuilder.withChannel(channel);
|
thingBuilder.withChannel(channel);
|
||||||
|
|||||||
@ -22,7 +22,9 @@ import java.util.Collections;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.openhab.core.library.types.DecimalType;
|
import org.openhab.core.library.types.DecimalType;
|
||||||
import org.openhab.core.library.types.OnOffType;
|
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.types.StringType;
|
||||||
|
import org.openhab.core.library.unit.SIUnits;
|
||||||
import org.openhab.core.thing.ThingStatus;
|
import org.openhab.core.thing.ThingStatus;
|
||||||
import org.snmp4j.PDU;
|
import org.snmp4j.PDU;
|
||||||
import org.snmp4j.Snmp;
|
import org.snmp4j.Snmp;
|
||||||
@ -109,6 +111,19 @@ public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest {
|
|||||||
verifyStatus(ThingStatus.ONLINE);
|
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
|
@Test
|
||||||
public void testCancelingAsyncRequest() {
|
public void testCancelingAsyncRequest() {
|
||||||
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.FLOAT);
|
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.FLOAT);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user