[snmp] Upgrades and enhancements (#14330)

* [snmp] Upgrades and enhancements

- bug: improve test stability
- enhancement: add support for UoM
- bug: fix misleading error message
- bug: fix initialization exceptions
- enhancement: Add support for SNMPv3
- enhancement: add opaque value handling

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2023-02-04 15:47:30 +01:00 committed by GitHub
parent 8a4033c95f
commit fc57f02fca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 809 additions and 178 deletions

View File

@ -308,7 +308,7 @@
/bundles/org.openhab.binding.smhi/ @pacive
/bundles/org.openhab.binding.smsmodem/ @dalgwen
/bundles/org.openhab.binding.sncf/ @clinique
/bundles/org.openhab.binding.snmp/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.snmp/ @J-N-K
/bundles/org.openhab.binding.solaredge/ @alexf2015
/bundles/org.openhab.binding.solarlog/ @johannrichard
/bundles/org.openhab.binding.solarmax/ @jamietownsend

View File

@ -2,12 +2,16 @@
This binding integrates the Simple Network Management Protocol (SNMP).
SNMP can be used to monitor or control a large variety of network equipment, e.g. routers, switches, NAS-systems.
Currently protocol version 1 and 2c are supported.
Currently, protocol version 1 and 2c are supported.
## Supported Things
Only one thing is supported: `target`.
It represents a single network device.
There are two supported things:
- `target` for SNMP v1/v2c agents
- `target3` for SNMP v3 agents
Both represent a single network device.
Things can be extended with `number`, `string` and `switch` channels.
## Binding Configuration
@ -17,7 +21,7 @@ In this case the `port` parameter defaults to `0`.
For receiving traps a port for receiving traps needs to be configured.
The standard port for receiving traps is 162, however binding to ports lower than 1024 is only allowed with privileged right on most *nix systems.
Therefore it is recommended to bind to a port higher than 1024 (e.g. 8162).
Therefore, it is recommended to bind to a port higher than 1024 (e.g. 8162).
In case the trap sending equipment does not allow to change the destination port (e.g. Mikrotik routers), it is necessary to forward the received packets to the new port.
This can be done either by software like _snmptrapd_ or by adding a firewall rule to your system, e.g. by executing
@ -40,19 +44,11 @@ port=8162
## Thing Configuration
The `target` thing has one mandatory parameter: `hostname`.
It can be set as FQDN or IP address.
### Common parameters for all thing-types
Optional configuration parameters are `community`, `version` and `refresh`.
The SNMP community can be set with the `community` parameter.
It defaults to `public`.
Currently two protocol versions are supported.
The protocol version can be set with the `protocol` parameter.
The allowed values are `v1` or `V1`for v1 and `v2c` or `V2C` for v2c.
The default is `v1`.
The `hostname` is mandatory and can be set as FQDN or IP address.
An optional configuration parameter is `refresh`.
By using the `refresh` parameter the time between two subsequent GET requests to the target can be set.
The default is `60` for 60s.
@ -67,6 +63,44 @@ A single request times out after `timeout` ms.
After `retries` timeouts the refresh operation is considered to be fails and the status of the thing set accordingly.
The default values are `timeout=1500` and `retries=2`.
### `target`
The `target` thing has two optional configuration parameters: `community` and `version`.
The SNMP community for SNMP version 2c can be set with the `community` parameter.
It defaults to `public`.
Currently two protocol versions are supported.
The protocol version can be set with the `protocol` parameter.
The allowed values are `v1` or `V1` for v1 and `v2c` or `V2C` for v2c.
The default is `v1`.
### `target3`
The `target3` thing has additional mandatory parameters: `engineId` and `user`.
The `engineId` must be given in hexadecimal notation (case-insensitive) without separators (e.g. `80000009035c710dbcd9e6`).
The allowed length is 11 to 32 bytes (22 to 64 hex characters).
If you encounter problems, please check if your agent prefixes the set engine id (e.g. Mikrotik uses `80003a8c04` and appends the set value to that).
The `user` parameter is named "securityName" or "userName" in most agents.
Optional configuration parameters are: `securityModel`, `authProtocol`, `authPassphrase`, `privProtocol` and `privPassphrase`.
The `securityModel` can be set to
- `NO_AUTH_NO_PRIV` (default) - no encryption on authentication data, no encryption on transmitted data
- `AUTH_NO_PRIV` - encryption on authentication data, no encryption on transmitted data
- `AUTH_PRIV` - encryption on authentication data, encryption on transmitted data
Depending on the `securityModel` some of the other parameters are also mandatory.
If authentication encryption is required, at least `authPassphrase` needs to be set, while `authProtocol` has a default of `MD5`.
Other possible values for `authProtocol` are `SHA`, `HMAC128SHA224`, `HMAC192SHA256`, `HMAC256SHA384` and `HMAC384SHA512`.
If encryption of transmitted data (privacy encryption) is required, at least `privPassphrase` needs to be set, while `privProtocol` defaults to `DES`.
Other possible values for `privProtocol` are `AES128`, `AES192` and `AES256`.
## Channels
The `target` thing has no fixed channels.
@ -87,7 +121,7 @@ Using`TRAP` channels requires configuring the receiving port (see "Binding confi
The `datatype` parameter is needed in some special cases where data is written to the target.
The default `datatype` for `number` channels is `UINT32`, representing an unsigned integer with 32 bit length.
Alternatively `INT32` (signed integer with 32 bit length), `COUNTER64` (unsigned integer with 64 bit length) or `FLOAT` (floating point number) can be set.
Floating point numbers have to be supplied (and will be send) as strings.
Floating point numbers have to be supplied (and will be sent) as strings.
For `string` channels the default `datatype` is `STRING` (i.e. the item's will be sent as a string).
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).
@ -99,12 +133,17 @@ In `READ`, `READ_WRITE` or `TRAP` mode they change to either `ON` or `OFF` on th
The parameters used for defining the values are `onvalue` and `offvalue`.
The `datatype` parameter is used to convert the configuration strings to the needed values.
`number`-type channels have a `unit` parameter.
The unit is added to the received value before it is passed to the channel.
For commands (i.e. sending), the value is first converted to the configured unit.
| type | item | description |
| ------ | ------ | ------------------------------ |
|----------|--------|---------------------------------|
| number | Number | a channel with a numeric value |
| string | String | a channel with a string value |
| switch | Switch | a channel that has two states |
### SNMP Exception (Error) Handling
The standard behaviour if an SNMP exception occurs this is to log at `INFO` level and set the channel value to `UNDEF`.
@ -121,10 +160,10 @@ Valid values are all valid values for that channel (i.e. `ON`/`OFF` for a switch
demo.things:
```java
```
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", unit="B" ]
Type number : inBytes [ oid=".1.3.6.1.2.1.31.1.1.1.6.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 switch : if4Command [ oid="1.3.6.1.2.1.2.2.1.7.4", mode="READ_WRITE", datatype="UINT32", onvalue="2", offvalue="0" ]
@ -137,7 +176,6 @@ demo.items:
```java
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

@ -29,6 +29,7 @@ public class SnmpBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_TARGET = new ThingTypeUID(BINDING_ID, "target");
public static final ThingTypeUID THING_TYPE_TARGET3 = new ThingTypeUID(BINDING_ID, "target3");
public static final ChannelTypeUID CHANNEL_TYPE_UID_NUMBER = new ChannelTypeUID(BINDING_ID, "number");
public static final ChannelTypeUID CHANNEL_TYPE_UID_STRING = new ChannelTypeUID(BINDING_ID, "string");

View File

@ -13,8 +13,8 @@
package org.openhab.binding.snmp.internal;
import static org.openhab.binding.snmp.internal.SnmpBindingConstants.THING_TYPE_TARGET;
import static org.openhab.binding.snmp.internal.SnmpBindingConstants.THING_TYPE_TARGET3;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -37,7 +37,7 @@ import org.osgi.service.component.annotations.Reference;
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.snmp")
public class SnmpHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_TARGET);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_TARGET, THING_TYPE_TARGET3);
private final SnmpService snmpService;
@ -54,7 +54,7 @@ public class SnmpHandlerFactory extends BaseThingHandlerFactory {
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_TARGET.equals(thingTypeUID)) {
if (THING_TYPE_TARGET.equals(thingTypeUID) || THING_TYPE_TARGET3.equals(thingTypeUID)) {
return new SnmpTargetHandler(thing, snmpService);
}
return null;

View File

@ -16,6 +16,8 @@ import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.types.SnmpAuthProtocol;
import org.openhab.binding.snmp.internal.types.SnmpPrivProtocol;
import org.snmp4j.CommandResponder;
import org.snmp4j.PDU;
import org.snmp4j.Target;
@ -31,9 +33,12 @@ import org.snmp4j.event.ResponseListener;
@NonNullByDefault
public interface SnmpService {
public void addCommandResponder(CommandResponder listener);
void addCommandResponder(CommandResponder listener);
public void removeCommandResponder(CommandResponder listener);
void removeCommandResponder(CommandResponder listener);
public void send(PDU pdu, Target target, @Nullable Object userHandle, ResponseListener listener) throws IOException;
void send(PDU pdu, Target target, @Nullable Object userHandle, ResponseListener listener) throws IOException;
void addUser(String userName, SnmpAuthProtocol snmpAuthProtocol, @Nullable String authPassphrase,
SnmpPrivProtocol snmpPrivProtocol, @Nullable String privPassphrase, byte[] engineId);
}

View File

@ -14,12 +14,16 @@ package org.openhab.binding.snmp.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.config.SnmpServiceConfiguration;
import org.openhab.binding.snmp.internal.types.SnmpAuthProtocol;
import org.openhab.binding.snmp.internal.types.SnmpPrivProtocol;
import org.openhab.core.config.core.Configuration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@ -32,8 +36,13 @@ import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.Target;
import org.snmp4j.event.ResponseListener;
import org.snmp4j.mp.MPv3;
import org.snmp4j.security.Priv3DES;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.DefaultUdpTransportMapping;
@ -53,10 +62,18 @@ public class SnmpServiceImpl implements SnmpService {
private @Nullable Snmp snmp;
private @Nullable DefaultUdpTransportMapping transport;
private List<CommandResponder> listeners = new ArrayList<>();
private final List<CommandResponder> listeners = new ArrayList<>();
private final Set<UserEntry> userEntries = new HashSet<>();
@Activate
public SnmpServiceImpl(Map<String, Object> config) {
SecurityProtocols.getInstance().addDefaultProtocols();
SecurityProtocols.getInstance().addPrivacyProtocol(new Priv3DES());
OctetString localEngineId = new OctetString(MPv3.createLocalEngineID());
USM usm = new USM(SecurityProtocols.getInstance(), localEngineId, 0);
SecurityModels.getInstance().addSecurityModel(usm);
modified(config);
}
@ -78,9 +95,12 @@ public class SnmpServiceImpl implements SnmpService {
SecurityProtocols.getInstance().addPrivacyProtocol(new Priv3DES());
final Snmp snmp = new Snmp(transport);
listeners.forEach(listener -> snmp.addCommandResponder(listener));
listeners.forEach(snmp::addCommandResponder);
snmp.listen();
// re-add user entries
userEntries.forEach(u -> addUser(snmp, u));
this.snmp = snmp;
this.transport = transport;
@ -90,6 +110,7 @@ public class SnmpServiceImpl implements SnmpService {
}
}
@SuppressWarnings("unused")
@Deactivate
public void deactivate() {
try {
@ -141,4 +162,37 @@ public class SnmpServiceImpl implements SnmpService {
logger.warn("SNMP service not initialized, can't send {} to {}", pdu, target);
}
}
@Override
public void addUser(String userName, SnmpAuthProtocol snmpAuthProtocol, @Nullable String authPassphrase,
SnmpPrivProtocol snmpPrivProtocol, @Nullable String privPassphrase, byte[] engineId) {
UsmUser usmUser = new UsmUser(new OctetString(userName), snmpAuthProtocol.getOid(),
authPassphrase != null ? new OctetString(authPassphrase) : null, snmpPrivProtocol.getOid(),
privPassphrase != null ? new OctetString(privPassphrase) : null);
OctetString securityNameOctets = new OctetString(userName);
UserEntry userEntry = new UserEntry(securityNameOctets, new OctetString(engineId), usmUser);
userEntries.add(userEntry);
Snmp snmp = this.snmp;
if (snmp != null) {
addUser(snmp, userEntry);
}
}
private static void addUser(Snmp snmp, UserEntry userEntry) {
snmp.getUSM().addUser(userEntry.securityName, userEntry.engineId, userEntry.user);
}
private static class UserEntry {
public OctetString securityName;
public OctetString engineId;
public UsmUser user;
public UserEntry(OctetString securityName, OctetString engineId, UsmUser user) {
this.securityName = securityName;
this.engineId = engineId;
this.user = user;
}
}
}

View File

@ -15,7 +15,6 @@ 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;
@ -28,13 +27,16 @@ 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;
import org.openhab.binding.snmp.internal.config.SnmpInternalChannelConfiguration;
import org.openhab.binding.snmp.internal.config.SnmpTargetConfiguration;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
import org.openhab.binding.snmp.internal.types.SnmpProtocolVersion;
import org.openhab.binding.snmp.internal.types.SnmpSecurityModel;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
@ -51,6 +53,7 @@ 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.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.AbstractTarget;
@ -59,7 +62,9 @@ import org.snmp4j.CommandResponderEvent;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.PDUv1;
import org.snmp4j.ScopedPDU;
import org.snmp4j.Snmp;
import org.snmp4j.UserTarget;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.event.ResponseListener;
import org.snmp4j.mp.SnmpConstants;
@ -68,6 +73,7 @@ import org.snmp4j.smi.Integer32;
import org.snmp4j.smi.IpAddress;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Opaque;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.smi.UnsignedInteger32;
import org.snmp4j.smi.Variable;
@ -114,11 +120,13 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
if (command instanceof RefreshType) {
SnmpInternalChannelConfiguration channel = readChannelSet.stream()
.filter(c -> channelUID.equals(c.channelUID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
PDU pdu = new PDU(PDU.GET, Collections.singletonList(new VariableBinding(channel.oid)));
.orElseThrow(() -> new IllegalArgumentException("no readable channel found"));
PDU pdu = getPDU();
pdu.setType(PDU.GET);
pdu.add(new VariableBinding(channel.oid));
snmpService.send(pdu, target, null, this);
} else if (command instanceof DecimalType || command instanceof StringType
|| command instanceof OnOffType) {
} else if (command instanceof DecimalType || command instanceof QuantityType
|| command instanceof StringType || command instanceof OnOffType) {
SnmpInternalChannelConfiguration channel = writeChannelSet.stream()
.filter(config -> channelUID.equals(config.channelUID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
@ -130,9 +138,25 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
return;
}
} else {
variable = convertDatatype(command, channel.datatype);
Command rawValue = command;
if (command instanceof QuantityType) {
Unit<?> channelUnit = channel.unit;
if (channelUnit == null) {
rawValue = new DecimalType(((QuantityType<?>) command).toBigDecimal());
} else {
QuantityType<?> convertedValue = ((QuantityType<?>) command).toUnit(channelUnit);
if (convertedValue == null) {
logger.warn("Cannot convert '{}' to configured unit '{}'", command, channelUnit);
return;
}
PDU pdu = new PDU(PDU.SET, Collections.singletonList(new VariableBinding(channel.oid, variable)));
rawValue = new DecimalType(convertedValue.toBigDecimal());
}
}
variable = convertDatatype(rawValue, channel.datatype);
}
PDU pdu = getPDU();
pdu.setType(PDU.SET);
pdu.add(new VariableBinding(channel.oid, variable));
snmpService.send(pdu, target, null, this);
}
} catch (IllegalArgumentException e) {
@ -148,22 +172,70 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
generateChannelConfigs();
if (thing.getThingTypeUID().equals(THING_TYPE_TARGET3)) {
// override default for target3 things
config.protocol = SnmpProtocolVersion.v3;
}
try {
if (config.protocol.toInteger() == SnmpConstants.version1
|| config.protocol.toInteger() == SnmpConstants.version2c) {
CommunityTarget target = new CommunityTarget();
target.setCommunity(new OctetString(config.community));
target.setRetries(config.retries);
target.setTimeout(config.timeout);
target.setVersion(config.protocol.toInteger());
target.setAddress(null);
this.target = target;
snmpService.addCommandResponder(this);
} else if (config.protocol.toInteger() == SnmpConstants.version3) {
String userName = config.user;
if (userName == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "user not set");
return;
}
String engineIdHexString = config.engineId;
if (engineIdHexString == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "engineId not set");
return;
}
String authPassphrase = config.authPassphrase;
if ((config.securityModel == SnmpSecurityModel.AUTH_PRIV
|| config.securityModel == SnmpSecurityModel.AUTH_NO_PRIV)
&& (authPassphrase == null || authPassphrase.isEmpty())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Authentication passphrase not configured");
return;
}
String privPassphrase = config.privPassphrase;
if (config.securityModel == SnmpSecurityModel.AUTH_PRIV
&& (privPassphrase == null || privPassphrase.isEmpty())) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Privacy passphrase not configured");
return;
}
byte[] engineId = HexUtils.hexToBytes(engineIdHexString);
snmpService.addUser(userName, config.authProtocol, authPassphrase, config.privProtocol, privPassphrase,
engineId);
UserTarget target = new UserTarget();
target.setAuthoritativeEngineID(engineId);
target.setSecurityName(new OctetString(config.user));
target.setSecurityLevel(config.securityModel.getSecurityLevel());
this.target = target;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SNMP version not supported");
return;
}
snmpService.addCommandResponder(this);
target.setRetries(config.retries);
target.setTimeout(config.timeout);
target.setVersion(config.protocol.toInteger());
target.setAddress(null);
timeoutCounter = 0;
} catch (IllegalArgumentException e) {
// some methods of SNMP4J throw an unchecked IllegalArgumentException if they receive invalid values
String message = "Exception during initialization: " + e.getMessage();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
return;
}
updateStatus(ThingStatus.UNKNOWN);
refresh = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS);
@ -230,9 +302,8 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
final String address = ((UdpAddress) event.getPeerAddress()).getInetAddress().getHostAddress();
final String community = new String(event.getSecurityName());
if ((pdu.getType() == PDU.V1TRAP) && config.community.equals(community) && (pdu instanceof PDUv1)) {
if ((pdu.getType() == PDU.V1TRAP) && config.community.equals(community) && (pdu instanceof PDUv1 pduv1)) {
logger.trace("{} received trap is PDUv1.", thing.getUID());
PDUv1 pduv1 = (PDUv1) pdu;
OID oidEnterprise = pduv1.getEnterprise();
int trapValue = pduv1.getGenericTrap();
if (trapValue == PDUv1.ENTERPRISE_SPECIFIC) {
@ -262,8 +333,8 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
SnmpDatatype datatype = config.datatype; // maybe null, override later
Variable onValue = null;
Variable offValue = null;
State exceptionValue = UnDefType.UNDEF;
Unit<?> unit = null;
State exceptionValue = UnDefType.UNDEF;
if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
if (datatype == null) {
@ -275,15 +346,11 @@ 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());
}
String configUnit = config.unit;
if (configUnit != null) {
unit = UnitUtils.parseUnit(configUnit);
if (unit == null) {
logger.warn("Failed to parse unit from '{}'for channel '{}'", unit, channel.getUID());
}
}
} else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
@ -323,13 +390,12 @@ 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, unit);
offValue, exceptionValue, unit, config.doNotLogException);
}
private void generateChannelConfigs() {
Set<SnmpInternalChannelConfiguration> channelConfigs = Collections
.unmodifiableSet(thing.getChannels().stream().map(channel -> getChannelConfigFromChannel(channel))
.filter(Objects::nonNull).collect(Collectors.toSet()));
Set<SnmpInternalChannelConfiguration> channelConfigs = Collections.unmodifiableSet(thing.getChannels().stream()
.map(this::getChannelConfigFromChannel).filter(Objects::nonNull).collect(Collectors.toSet()));
this.readChannelSet = channelConfigs.stream()
.filter(c -> c.mode == SnmpChannelMode.READ || c.mode == SnmpChannelMode.READ_WRITE)
.collect(Collectors.toSet());
@ -359,17 +425,37 @@ 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) {
numericState = new BigDecimal(value.toString());
} else {
numericState = BigDecimal.valueOf(value.toLong());
if (value instanceof Opaque opaque) {
byte[] octets = opaque.toByteArray();
if (octets.length < 3) {
// two bytes identifier and one byte length should always be present
throw new UnsupportedOperationException("Not enough octets");
}
if (unit != null) {
state = new QuantityType<>(numericState, unit);
if (octets.length != (3 + octets[2])) {
// octet 3 contains the lengths of the value
throw new UnsupportedOperationException("Not enough octets");
}
if (octets[0] == (byte) 0x9f && octets[1] == 0x78 && octets[2] == 0x04) {
// floating point value
Unit<?> channelUnit = channelConfig.unit;
float floatValue = Float.intBitsToFloat(
octets[3] << 24 | octets[4] << 16 | octets[5] << 8 | octets[6]);
state = channelUnit == null ? new DecimalType(floatValue)
: new QuantityType<>(floatValue, channelUnit);
} else {
state = new DecimalType(numericState);
throw new UnsupportedOperationException("Unknown opaque datatype" + value);
}
} else {
Unit<?> channelUnit = channelConfig.unit;
state = channelUnit == null ? new DecimalType(value.toString())
: new QuantityType<>(value + channelUnit.getSymbol());
}
} else {
Unit<?> channelUnit = channelConfig.unit;
state = channelUnit == null ? new DecimalType(value.toLong())
: new QuantityType<>(value.toLong(), channelUnit);
}
} catch (UnsupportedOperationException e) {
logger.warn("could not convert {} to number for channel {}", value, channelUID);
@ -404,36 +490,35 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
private Variable convertDatatype(Command command, SnmpDatatype datatype) {
switch (datatype) {
case INT32:
case INT32 -> {
if (command instanceof DecimalType) {
return new Integer32(((DecimalType) command).intValue());
} else if (command instanceof StringType) {
return new Integer32((new DecimalType(((StringType) command).toString())).intValue());
}
break;
case UINT32:
}
case UINT32 -> {
if (command instanceof DecimalType) {
return new UnsignedInteger32(((DecimalType) command).intValue());
} else if (command instanceof StringType) {
return new UnsignedInteger32((new DecimalType(((StringType) command).toString())).intValue());
}
break;
case COUNTER64:
}
case COUNTER64 -> {
if (command instanceof DecimalType) {
return new Counter64(((DecimalType) command).longValue());
} else if (command instanceof StringType) {
return new Counter64((new DecimalType(((StringType) command).toString())).longValue());
}
break;
case FLOAT:
case STRING:
}
case FLOAT, STRING -> {
if (command instanceof DecimalType) {
return new OctetString(((DecimalType) command).toString());
} else if (command instanceof StringType) {
return new OctetString(((StringType) command).toString());
}
break;
case HEXSTRING:
}
case HEXSTRING -> {
if (command instanceof StringType) {
String commandString = ((StringType) command).toString().toLowerCase();
Matcher commandMatcher = HEXSTRING_VALIDITY.matcher(commandString);
@ -442,13 +527,14 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
return OctetString.fromHexStringPairs(commandString);
}
}
break;
case IPADDRESS:
}
case IPADDRESS -> {
if (command instanceof StringType) {
return new IpAddress(((StringType) command).toString());
}
break;
default:
}
default -> {
}
}
throw new IllegalArgumentException("illegal conversion of " + command + " to " + datatype);
}
@ -472,8 +558,9 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
return;
}
}
PDU pdu = new PDU(PDU.GET,
readChannelSet.stream().map(c -> new VariableBinding(c.oid)).collect(Collectors.toList()));
PDU pdu = getPDU();
pdu.setType(PDU.GET);
readChannelSet.stream().map(c -> new VariableBinding(c.oid)).forEach(pdu::add);
if (!pdu.getVariableBindings().isEmpty()) {
try {
snmpService.send(pdu, target, null, this);
@ -482,4 +569,12 @@ public class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
}
}
}
private PDU getPDU() {
if (config.protocol == SnmpProtocolVersion.v3 || config.protocol == SnmpProtocolVersion.V3) {
return new ScopedPDU();
} else {
return new PDU();
}
}
}

View File

@ -14,8 +14,8 @@ package org.openhab.binding.snmp.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.SnmpChannelMode;
import org.openhab.binding.snmp.internal.SnmpDatatype;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
/**
* The {@link SnmpChannelConfiguration} class contains fields mapping channel configuration parameters.
@ -27,12 +27,11 @@ public class SnmpChannelConfiguration {
public @Nullable String oid;
public SnmpChannelMode mode = SnmpChannelMode.READ;
public @Nullable SnmpDatatype datatype;
public @Nullable String unit;
public @Nullable String onvalue;
public @Nullable String offvalue;
public @Nullable String exceptionValue;
public boolean doNotLogException = false;
public @Nullable String unit;
}

View File

@ -16,8 +16,8 @@ import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.SnmpChannelMode;
import org.openhab.binding.snmp.internal.SnmpDatatype;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.snmp4j.smi.OID;
@ -39,12 +39,12 @@ public class SnmpInternalChannelConfiguration {
public final @Nullable Variable onValue;
public final @Nullable Variable offValue;
public final State exceptionValue;
public final boolean doNotLogException;
public final @Nullable Unit<?> unit;
public final boolean doNotLogException;
public SnmpInternalChannelConfiguration(ChannelUID channelUID, OID oid, SnmpChannelMode mode, SnmpDatatype datatype,
@Nullable Variable onValue, @Nullable Variable offValue, State exceptionValue, boolean doNotLogException,
@Nullable Unit<?> unit) {
@Nullable Variable onValue, @Nullable Variable offValue, State exceptionValue, @Nullable Unit<?> unit,
boolean doNotLogException) {
this.channelUID = channelUID;
this.oid = oid;
this.mode = mode;
@ -52,7 +52,7 @@ public class SnmpInternalChannelConfiguration {
this.onValue = onValue;
this.offValue = offValue;
this.exceptionValue = exceptionValue;
this.doNotLogException = doNotLogException;
this.unit = unit;
this.doNotLogException = doNotLogException;
}
}

View File

@ -14,7 +14,10 @@ package org.openhab.binding.snmp.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.snmp.internal.SnmpProtocolVersion;
import org.openhab.binding.snmp.internal.types.SnmpAuthProtocol;
import org.openhab.binding.snmp.internal.types.SnmpPrivProtocol;
import org.openhab.binding.snmp.internal.types.SnmpProtocolVersion;
import org.openhab.binding.snmp.internal.types.SnmpSecurityModel;
/**
* The {@link SnmpTargetConfiguration} class contains fields mapping thing configuration parameters.
@ -23,11 +26,24 @@ import org.openhab.binding.snmp.internal.SnmpProtocolVersion;
*/
@NonNullByDefault
public class SnmpTargetConfiguration {
// common
public @Nullable String hostname;
public int port = 161;
public String community = "public";
public int refresh = 60;
public SnmpProtocolVersion protocol = SnmpProtocolVersion.v1;
public int refresh = 60;
public int timeout = 1500;
public int retries = 2;
// v1/v2c only
public String community = "public";
// v3 only
public SnmpSecurityModel securityModel = SnmpSecurityModel.NO_AUTH_NO_PRIV;
public @Nullable String user;
public @Nullable String engineId;
public SnmpAuthProtocol authProtocol = SnmpAuthProtocol.MD5;
public @Nullable String authPassphrase;
public SnmpPrivProtocol privProtocol = SnmpPrivProtocol.DES;
public @Nullable String privPassphrase;
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2023 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.snmp.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.snmp4j.security.AuthHMAC128SHA224;
import org.snmp4j.security.AuthHMAC192SHA256;
import org.snmp4j.security.AuthHMAC256SHA384;
import org.snmp4j.security.AuthHMAC384SHA512;
import org.snmp4j.security.AuthMD5;
import org.snmp4j.security.AuthSHA;
import org.snmp4j.smi.OID;
/**
* The {@link SnmpAuthProtocol} enum defines the possible authentication protocols for v3
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum SnmpAuthProtocol {
MD5(AuthMD5.ID),
SHA(AuthSHA.ID),
HMAC128SHA224(AuthHMAC128SHA224.ID),
HMAC192SHA256(AuthHMAC192SHA256.ID),
HMAC256SHA384(AuthHMAC256SHA384.ID),
HMAC384SHA512(AuthHMAC384SHA512.ID);
private final OID oid;
SnmpAuthProtocol(OID oid) {
this.oid = oid;
}
/**
* get the OID for this authentication protocol
*
* @return the corresponding OID
*/
public OID getOid() {
return oid;
}
}

View File

@ -10,14 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.snmp.internal;
package org.openhab.binding.snmp.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SnmpChannelMode} enum defines the mode of SNMP channels
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum SnmpChannelMode {
READ,
WRITE,

View File

@ -10,14 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.snmp.internal;
package org.openhab.binding.snmp.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SnmpDatatype} enum defines the datatype of SNMP channels
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum SnmpDatatype {
INT32,
UINT32,

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.snmp.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.snmp4j.security.PrivAES128;
import org.snmp4j.security.PrivAES192;
import org.snmp4j.security.PrivAES256;
import org.snmp4j.security.PrivDES;
import org.snmp4j.smi.OID;
/**
* The {@link SnmpPrivProtocol} enum defines the possible privacy protocols for v3
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum SnmpPrivProtocol {
AES128(PrivAES128.ID),
AES192(PrivAES192.ID),
AES256(PrivAES256.ID),
DES(PrivDES.ID);
private final OID oid;
SnmpPrivProtocol(OID oid) {
this.oid = oid;
}
/**
* get the OID for this privacy protocol
*
* @return the corresponding OID
*/
public OID getOid() {
return oid;
}
}

View File

@ -10,23 +10,27 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.snmp.internal;
package org.openhab.binding.snmp.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SnmpProtocolVersion} enum defines the datatype of SNMP channels
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum SnmpProtocolVersion {
v1(0),
V1(0),
v2c(1),
V2C(1);
V2C(1),
v3(3),
V3(3);
private final int value;
private SnmpProtocolVersion(int value) {
SnmpProtocolVersion(int value) {
this.value = value;
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2023 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.snmp.internal.types;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.snmp4j.security.SecurityLevel;
/**
* The {@link SnmpSecurityModel} enum defines the security model for v3
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum SnmpSecurityModel {
NO_AUTH_NO_PRIV(SecurityLevel.NOAUTH_NOPRIV),
AUTH_NO_PRIV(SecurityLevel.AUTH_NOPRIV),
AUTH_PRIV(SecurityLevel.AUTH_PRIV);
private final int securityLevel;
SnmpSecurityModel(int securityLevel) {
this.securityLevel = securityLevel;
}
/**
* get the numeric security level
*
* @return the int representing this security level
*/
public int getSecurityLevel() {
return securityLevel;
}
}

View File

@ -11,6 +11,7 @@ addon.config.snmp.port.description = Port for receiving traps, set to 0 to disab
# thing types
thing-type.snmp.target.label = SNMP Target
thing-type.snmp.target3.label = SNMP v3 Target
# thing types config
@ -27,6 +28,36 @@ thing-type.config.snmp.target.retries.label = Retries
thing-type.config.snmp.target.retries.description = Number of retries for an update request
thing-type.config.snmp.target.timeout.label = Timeout
thing-type.config.snmp.target.timeout.description = Timeout in ms for a single update request
thing-type.config.snmp.target3.authPassphrase.label = Authentication Passphrase
thing-type.config.snmp.target3.authProtocol.label = Authentication Protocol
thing-type.config.snmp.target3.authProtocol.option.MD5 = MD5
thing-type.config.snmp.target3.authProtocol.option.SHA = SHA
thing-type.config.snmp.target3.authProtocol.option.HMAC128SHA224 = HMAC128SHA224
thing-type.config.snmp.target3.authProtocol.option.HMAC192SHA256 = HMAC192SHA256
thing-type.config.snmp.target3.authProtocol.option.HMAC256SHA384 = HMAC256SHA384
thing-type.config.snmp.target3.authProtocol.option.HMAC384SHA512 = HMAC384SHA512
thing-type.config.snmp.target3.engineId.label = Engine ID
thing-type.config.snmp.target3.engineId.description = The authorization engine ID of this target in hexadecimal notation (22-64 characters)
thing-type.config.snmp.target3.hostname.label = Target Host
thing-type.config.snmp.target3.hostname.description = Hostname or IP address of target host
thing-type.config.snmp.target3.port.label = Port
thing-type.config.snmp.target3.privPassphrase.label = Privacy Passphrase
thing-type.config.snmp.target3.privProtocol.label = Privacy Protocol
thing-type.config.snmp.target3.privProtocol.option.AES128 = AES128
thing-type.config.snmp.target3.privProtocol.option.AES192 = AES192
thing-type.config.snmp.target3.privProtocol.option.AES256 = AES256
thing-type.config.snmp.target3.privProtocol.option.DES = DES
thing-type.config.snmp.target3.refresh.label = Refresh Time
thing-type.config.snmp.target3.refresh.description = Refresh time in s (default 60s)
thing-type.config.snmp.target3.retries.label = Retries
thing-type.config.snmp.target3.retries.description = Number of retries for an update request
thing-type.config.snmp.target3.securityModel.label = Security Model
thing-type.config.snmp.target3.securityModel.option.NO_AUTH_NO_PRIV = No authentication and no Privacy
thing-type.config.snmp.target3.securityModel.option.AUTH_NO_PRIV = Authentication and no Privacy
thing-type.config.snmp.target3.securityModel.option.AUTH_PRIV = Authentication and Privacy
thing-type.config.snmp.target3.timeout.label = Timeout
thing-type.config.snmp.target3.timeout.description = Timeout in ms for a single update request
thing-type.config.snmp.target3.user.label = Username
# channel types

View File

@ -52,7 +52,92 @@
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<thing-type id="target3" extensible="number,string,switch">
<label>SNMP v3 Target</label>
<config-description>
<!-- required -->
<parameter name="hostname" type="text" required="true">
<label>Target Host</label>
<description>Hostname or IP address of target host</description>
<context>network-address</context>
</parameter>
<parameter name="engineId" type="text" required="true">
<label>Engine ID</label>
<description>The authorization engine ID of this target in hexadecimal notation (22-64 characters)</description>
</parameter>
<parameter name="user" type="text" required="true">
<label>Username</label>
</parameter>
<!-- optional -->
<parameter name="securityModel" type="text">
<label>Security Model</label>
<options>
<option value="NO_AUTH_NO_PRIV">No authentication and no Privacy</option>
<option value="AUTH_NO_PRIV">Authentication and no Privacy</option>
<option value="AUTH_PRIV">Authentication and Privacy</option>
</options>
<limitToOptions>true</limitToOptions>
<default>NO_AUTH_NO_PRIV</default>
</parameter>
<parameter name="authProtocol" type="text">
<label>Authentication Protocol</label>
<options>
<option value="MD5">MD5</option>
<option value="SHA">SHA</option>
<option value="HMAC128SHA224">HMAC128SHA224</option>
<option value="HMAC192SHA256">HMAC192SHA256</option>
<option value="HMAC256SHA384">HMAC256SHA384</option>
<option value="HMAC384SHA512">HMAC384SHA512</option>
</options>
<limitToOptions>true</limitToOptions>
<default>MD5</default>
</parameter>
<parameter name="authPassphrase" type="text">
<label>Authentication Passphrase</label>
<context>password</context>
</parameter>
<parameter name="privProtocol" type="text">
<label>Privacy Protocol</label>
<options>
<option value="AES128">AES128</option>
<option value="AES192">AES192</option>
<option value="AES256">AES256</option>
<option value="DES">DES</option>
</options>
<limitToOptions>true</limitToOptions>
<default>DES</default>
</parameter>
<parameter name="privPassphrase" type="text">
<label>Privacy Passphrase</label>
<context>password</context>
</parameter>
<parameter name="refresh" type="integer" min="1">
<label>Refresh Time</label>
<description>Refresh time in s (default 60s)</description>
<default>60</default>
</parameter>
<!-- optional advanced -->
<parameter name="port" type="integer">
<label>Port</label>
<default>161</default>
<advanced>true</advanced>
</parameter>
<parameter name="timeout" type="integer" min="0">
<label>Timeout</label>
<description>Timeout in ms for a single update request</description>
<default>1500</default>
<advanced>true</advanced>
</parameter>
<parameter name="retries" type="integer" min="0">
<label>Retries</label>
<description>Number of retries for an update request</description>
<default>2</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="number">
@ -76,6 +161,10 @@
<default>READ</default>
<limitToOptions>true</limitToOptions>
</parameter>
<parameter name="unit" type="text">
<label>Unit</label>
<description>The unit of this value.</description>
</parameter>
<parameter name="datatype" type="text">
<label>Datatype</label>
<description>Content data type</description>
@ -99,12 +188,6 @@
<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

@ -23,10 +23,14 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.test.java.JavaTest;
@ -53,6 +57,7 @@ import org.snmp4j.smi.VariableBinding;
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
protected static final ThingUID THING_UID = new ThingUID(THING_TYPE_TARGET, "testthing");
protected static final ChannelUID CHANNEL_UID = new ChannelUID(THING_UID, "testchannel");
@ -60,20 +65,20 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
protected static final String TEST_ADDRESS = "192.168.0.1";
protected static final String TEST_STRING = "foo.";
protected @Mock SnmpServiceImpl snmpService;
protected @Mock ThingHandlerCallback thingHandlerCallback;
protected @Mock @NonNullByDefault({}) SnmpServiceImpl snmpService;
protected @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
protected Thing thing;
protected SnmpTargetHandler thingHandler;
private AutoCloseable mocks;
protected @NonNullByDefault({}) Thing thing;
protected @NonNullByDefault({}) SnmpTargetHandler thingHandler;
private @NonNullByDefault({}) AutoCloseable mocks;
@AfterEach
public void after() throws Exception {
mocks.close();
}
protected VariableBinding handleCommandSwitchChannel(SnmpDatatype datatype, Command command, String onValue,
String offValue, boolean refresh) throws IOException {
protected @Nullable VariableBinding handleCommandSwitchChannel(SnmpDatatype datatype, Command command,
String onValue, @Nullable String offValue, boolean refresh) throws IOException {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_SWITCH, SnmpChannelMode.WRITE, datatype, onValue, offValue);
thingHandler.handleCommand(CHANNEL_UID, command);
@ -87,9 +92,14 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
}
}
protected VariableBinding handleCommandNumberStringChannel(ChannelTypeUID channelTypeUID, SnmpDatatype datatype,
Command command, boolean refresh) throws IOException {
setup(channelTypeUID, SnmpChannelMode.WRITE, datatype);
protected @Nullable VariableBinding handleCommandNumberStringChannel(ChannelTypeUID channelTypeUID,
SnmpDatatype datatype, Command command, boolean refresh) throws IOException {
return handleCommandNumberStringChannel(channelTypeUID, datatype, null, command, refresh);
}
protected @Nullable VariableBinding handleCommandNumberStringChannel(ChannelTypeUID channelTypeUID,
SnmpDatatype datatype, @Nullable String unit, Command command, boolean refresh) throws IOException {
setup(channelTypeUID, SnmpChannelMode.WRITE, datatype, null, null, null, unit);
thingHandler.handleCommand(CHANNEL_UID, command);
if (refresh) {
@ -118,8 +128,8 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
}
}
protected State onResponseSwitchChannel(SnmpChannelMode channelMode, SnmpDatatype datatype, String onValue,
String offValue, Variable value, boolean refresh) {
protected @Nullable State onResponseSwitchChannel(SnmpChannelMode channelMode, SnmpDatatype datatype,
String onValue, String offValue, Variable value, boolean refresh) {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_SWITCH, channelMode, datatype, onValue, offValue);
PDU responsePDU = new PDU(PDU.RESPONSE,
@ -159,22 +169,23 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
setup(channelTypeUID, channelMode, null);
}
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype) {
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, @Nullable SnmpDatatype datatype) {
setup(channelTypeUID, channelMode, datatype, null, null);
}
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype,
String onValue, String offValue) {
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, @Nullable SnmpDatatype datatype,
@Nullable String onValue, @Nullable String offValue) {
setup(channelTypeUID, channelMode, datatype, onValue, offValue, null);
}
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, SnmpDatatype datatype,
String onValue, String offValue, String exceptionValue) {
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, @Nullable SnmpDatatype datatype,
@Nullable String onValue, @Nullable String offValue, @Nullable 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) {
protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, @Nullable SnmpDatatype datatype,
@Nullable String onValue, @Nullable String offValue, @Nullable String exceptionValue,
@Nullable String unit) {
Map<String, Object> channelConfig = new HashMap<>();
Map<String, Object> thingConfig = new HashMap<>();
mocks = MockitoAnnotations.openMocks(this);
@ -184,7 +195,6 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
ThingBuilder thingBuilder = ThingBuilder.create(THING_TYPE_TARGET, THING_UID).withLabel("Test thing")
.withConfiguration(new Configuration(thingConfig));
if (channelTypeUID != null && channelMode != null) {
String itemType = SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER.equals(channelTypeUID) ? "Number" : "String";
channelConfig.put("oid", TEST_OID);
channelConfig.put("mode", channelMode.name());
@ -206,7 +216,6 @@ public abstract class AbstractSnmpTargetHandlerTest extends JavaTest {
Channel channel = ChannelBuilder.create(CHANNEL_UID, itemType).withType(channelTypeUID)
.withConfiguration(new Configuration(channelConfig)).build();
thingBuilder.withChannel(channel);
}
thing = thingBuilder.build();
thingHandler = new SnmpTargetHandler(thing, snmpService);

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2023 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.snmp.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.verify;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ThingStatus;
import org.snmp4j.PDU;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.smi.Counter64;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.Opaque;
import org.snmp4j.smi.VariableBinding;
/**
* Tests cases for {@link SnmpTargetHandler}.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class NumberChannelTest extends AbstractSnmpTargetHandlerTest {
@Test
public void testNumberChannelsProperlyUpdatingOnOpaque() {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.FLOAT);
PDU responsePDU = new PDU(PDU.RESPONSE, List.of(new VariableBinding(new OID(TEST_OID),
new Opaque(new byte[] { (byte) 0x9f, 0x78, 0x04, 0x41, 0x5b, 0x33, 0x33 }))));
ResponseEvent event = new ResponseEvent("test", null, null, responsePDU, null);
thingHandler.onResponse(event);
final ArgumentCaptor<DecimalType> captor = ArgumentCaptor.forClass(DecimalType.class);
verify(thingHandlerCallback, atLeast(1)).stateUpdated(eq(CHANNEL_UID), captor.capture());
assertEquals(13.7, captor.getValue().doubleValue(), 0.001);
verifyStatus(ThingStatus.ONLINE);
}
@Test
public void testNumberChannelsProperlyUpdatingOnInteger() {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.COUNTER64);
PDU responsePDU = new PDU(PDU.RESPONSE,
List.of(new VariableBinding(new OID(TEST_OID), new Counter64(1234567891333L))));
ResponseEvent event = new ResponseEvent("test", null, null, responsePDU, null);
thingHandler.onResponse(event);
verify(thingHandlerCallback, atLeast(1)).stateUpdated(eq(CHANNEL_UID), eq(new DecimalType(1234567891333L)));
verifyStatus(ThingStatus.ONLINE);
}
@Test
public void testNumberChannelsProperlyUpdatingOnQuantityType() {
setup(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpChannelMode.READ, SnmpDatatype.FLOAT, null, null, null,
"°C");
PDU responsePDU = new PDU(PDU.RESPONSE, List.of(new VariableBinding(new OID(TEST_OID),
new Opaque(new byte[] { (byte) 0x9f, 0x78, 0x04, 0x41, 0x5b, 0x33, 0x33 }))));
ResponseEvent event = new ResponseEvent("test", null, null, responsePDU, null);
thingHandler.onResponse(event);
final ArgumentCaptor<QuantityType<?>> captor = ArgumentCaptor.forClass(QuantityType.class);
verify(thingHandlerCallback, atLeast(1)).stateUpdated(eq(CHANNEL_UID), captor.capture());
assertEquals(13.7, captor.getValue().doubleValue(), 0.001);
assertEquals(SIUnits.CELSIUS, captor.getValue().getUnit());
verifyStatus(ThingStatus.ONLINE);
}
}

View File

@ -19,12 +19,15 @@ import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
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;
@ -41,6 +44,7 @@ import org.snmp4j.smi.VariableBinding;
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest {
@Test
@ -52,7 +56,7 @@ public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest {
}
@Test
public void testChannelsProperlyUpdate() throws IOException {
public void testChannelsProperlyUpdate() {
onResponseNumberStringChannel(SnmpChannelMode.READ, true);
onResponseNumberStringChannel(SnmpChannelMode.READ_WRITE, true);
onResponseNumberStringChannel(SnmpChannelMode.WRITE, false);
@ -73,30 +77,66 @@ public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest {
VariableBinding variable;
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpDatatype.INT32,
new DecimalType(-5), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof Integer32);
assertEquals(-5, ((Integer32) variable.getVariable()).toInt());
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpDatatype.UINT32,
new DecimalType(10000), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof UnsignedInteger32);
assertEquals(10000, ((UnsignedInteger32) variable.getVariable()).toInt());
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER,
SnmpDatatype.COUNTER64, new DecimalType(10000), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof Counter64);
assertEquals(10000, ((Counter64) variable.getVariable()).toInt());
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpDatatype.FLOAT,
new DecimalType("12.4"), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof OctetString);
assertEquals("12.4", variable.getVariable().toString());
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpDatatype.FLOAT,
"°C", new QuantityType<>("50 °F"), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof OctetString);
assertEquals("10.00", variable.getVariable().toString().substring(0, 5));
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER, SnmpDatatype.INT32,
new StringType(TEST_STRING), false);
null, new StringType(TEST_STRING), false);
assertNull(variable);
}
@ -111,19 +151,6 @@ 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);
@ -139,11 +166,11 @@ public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest {
verifyStatus(ThingStatus.ONLINE);
}
class SnmpMock extends Snmp {
static class SnmpMock extends Snmp {
public int cancelCallCounter = 0;
@Override
public void cancel(PDU request, org.snmp4j.event.ResponseListener listener) {
public void cancel(@Nullable PDU request, org.snmp4j.event.@Nullable ResponseListener listener) {
++cancelCallCounter;
}
}

View File

@ -19,7 +19,10 @@ import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ThingStatus;
@ -35,6 +38,7 @@ import org.snmp4j.smi.VariableBinding;
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class StringChannelTest extends AbstractSnmpTargetHandlerTest {
@Test
@ -43,6 +47,12 @@ public class StringChannelTest extends AbstractSnmpTargetHandlerTest {
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_STRING, SnmpDatatype.STRING,
new StringType(TEST_STRING), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof OctetString);
assertEquals(TEST_STRING, ((OctetString) variable.getVariable()).toString());
@ -57,12 +67,24 @@ public class StringChannelTest extends AbstractSnmpTargetHandlerTest {
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_STRING,
SnmpDatatype.HEXSTRING, new StringType("AA bf 11"), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof OctetString);
assertEquals("aa bf 11", ((OctetString) variable.getVariable()).toHexString(' '));
variable = handleCommandNumberStringChannel(SnmpBindingConstants.CHANNEL_TYPE_UID_STRING,
SnmpDatatype.IPADDRESS, new StringType(TEST_ADDRESS), true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof IpAddress);
assertEquals(TEST_ADDRESS, ((IpAddress) variable.getVariable()).toString());

View File

@ -19,7 +19,10 @@ import static org.mockito.Mockito.*;
import java.io.IOException;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.snmp.internal.types.SnmpChannelMode;
import org.openhab.binding.snmp.internal.types.SnmpDatatype;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.State;
@ -38,6 +41,7 @@ import org.snmp4j.smi.VariableBinding;
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class SwitchChannelTest extends AbstractSnmpTargetHandlerTest {
@Test
@ -45,11 +49,23 @@ public class SwitchChannelTest extends AbstractSnmpTargetHandlerTest {
VariableBinding variable;
variable = handleCommandSwitchChannel(SnmpDatatype.STRING, OnOffType.ON, "on", "off", true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof OctetString);
assertEquals("on", ((OctetString) variable.getVariable()).toString());
variable = handleCommandSwitchChannel(SnmpDatatype.STRING, OnOffType.OFF, "on", "off", true);
if (variable == null) {
fail("'variable' is null");
return;
}
assertEquals(new OID(TEST_OID), variable.getOid());
assertTrue(variable.getVariable() instanceof OctetString);
assertEquals("off", ((OctetString) variable.getVariable()).toString());