From f67bfbe583eb60b2c650f4ceac805f66e3491809 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Fri, 3 Feb 2023 21:06:30 +0100 Subject: [PATCH] [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 --- bundles/org.openhab.binding.snmp/README.md | 84 +++++-- .../snmp/internal/SnmpBindingConstants.java | 1 + .../snmp/internal/SnmpHandlerFactory.java | 6 +- .../binding/snmp/internal/SnmpService.java | 11 +- .../snmp/internal/SnmpServiceImpl.java | 58 ++++- .../snmp/internal/SnmpTargetHandler.java | 213 +++++++++++++----- .../config/SnmpChannelConfiguration.java | 7 +- .../SnmpInternalChannelConfiguration.java | 12 +- .../config/SnmpTargetConfiguration.java | 22 +- .../snmp/internal/types/SnmpAuthProtocol.java | 52 +++++ .../internal/{ => types}/SnmpChannelMode.java | 6 +- .../internal/{ => types}/SnmpDatatype.java | 6 +- .../snmp/internal/types/SnmpPrivProtocol.java | 48 ++++ .../{ => types}/SnmpProtocolVersion.java | 12 +- .../internal/types/SnmpSecurityModel.java | 43 ++++ .../resources/OH-INF/thing/thing-types.xml | 95 +++++++- .../AbstractSnmpTargetHandlerTest.java | 91 ++++---- .../snmp/internal/NumberChannelTest.java | 84 +++++++ .../snmp/internal/SnmpTargetHandlerTest.java | 63 ++++-- .../snmp/internal/StringChannelTest.java | 22 ++ .../snmp/internal/SwitchChannelTest.java | 16 ++ .../internal/handler/SysteminfoHandler.java | 2 +- 22 files changed, 777 insertions(+), 177 deletions(-) create mode 100644 bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpAuthProtocol.java rename bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/{ => types}/SnmpChannelMode.java (83%) rename bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/{ => types}/SnmpDatatype.java (84%) create mode 100644 bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpPrivProtocol.java rename bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/{ => types}/SnmpProtocolVersion.java (78%) create mode 100644 bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpSecurityModel.java create mode 100644 bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/NumberChannelTest.java diff --git a/bundles/org.openhab.binding.snmp/README.md b/bundles/org.openhab.binding.snmp/README.md index a795fc7bb97e0..2065dfd6cf43f 100644 --- a/bundles/org.openhab.binding.snmp/README.md +++ b/bundles/org.openhab.binding.snmp/README.md @@ -6,8 +6,12 @@ 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 @@ -25,8 +29,8 @@ This can be done either by software like _snmptrapd_ or by adding a firewall rul iptables -t nat -I PREROUTING --src 0/0 --dst 192.168.0.10 -p udp --dport 162 -j REDIRECT --to-ports 8162 ``` -would forward all TCP packets addressed to 192.168.0.10 from port 162 to 8162. -Check with your operating system manual how to make that change permanent. +would forward all TCP packets addressed to 192.168.0.10 from port 162 to 8162. +Check with your operating system manual how to make that change permanent. Example configuration for using port 8162: @@ -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. - -Optional configuration parameters are `community`, `version` and `refresh`. - -The SNMP community can be set with the `community` parameter. -It defaults to `public`. +### Common parameters for all thing-types -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. +Optional configuration parameters 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 rquired, 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. @@ -99,11 +133,16 @@ 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. -| 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 | +`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 @@ -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" } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBindingConstants.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBindingConstants.java index 399a6ef6b1f4b..74ba891d64979 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBindingConstants.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBindingConstants.java @@ -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"); diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpHandlerFactory.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpHandlerFactory.java index b28895d58855b..6ee2a6047680b 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpHandlerFactory.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpHandlerFactory.java @@ -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 @@ @NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.snmp") public class SnmpHandlerFactory extends BaseThingHandlerFactory { - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_TARGET); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_TARGET, THING_TYPE_TARGET3); private final SnmpService snmpService; @@ -54,7 +54,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { @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; diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpService.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpService.java index 2f554445ec18c..17e87d18cbb3a 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpService.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpService.java @@ -16,6 +16,8 @@ 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 @@ @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); } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpServiceImpl.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpServiceImpl.java index 2aa9b55805e21..4f655a6140f05 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpServiceImpl.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpServiceImpl.java @@ -14,12 +14,16 @@ 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.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 listeners = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private final Set userEntries = new HashSet<>(); @Activate public SnmpServiceImpl(Map 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 @@ protected void modified(Map config) { 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 @@ protected void modified(Map config) { } } + @SuppressWarnings("unused") @Deactivate public void deactivate() { try { @@ -141,4 +162,37 @@ public void send(PDU pdu, Target target, @Nullable Object userHandle, ResponseLi 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; + } + } } 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 647ca8dfdda9c..6709a817a3144 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,7 +15,6 @@ 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.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.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.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.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 void handleCommand(ChannelUID channelUID, Command command) { 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 void handleCommand(ChannelUID channelUID, Command command) { 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; + } + rawValue = new DecimalType(convertedValue.toBigDecimal()); + } + } + variable = convertDatatype(rawValue, channel.datatype); } - PDU pdu = new PDU(PDU.SET, Collections.singletonList(new VariableBinding(channel.oid, variable))); + PDU pdu = getPDU(); + pdu.setType(PDU.SET); + pdu.add(new VariableBinding(channel.oid, variable)); snmpService.send(pdu, target, null, this); } } catch (IllegalArgumentException e) { @@ -148,23 +172,71 @@ public void initialize() { generateChannelConfigs(); - if (config.protocol.toInteger() == SnmpConstants.version1 - || config.protocol.toInteger() == SnmpConstants.version2c) { - CommunityTarget target = new CommunityTarget(); - target.setCommunity(new OctetString(config.community)); + 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)); + this.target = target; + } 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); - this.target = target; - snmpService.addCommandResponder(this); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SNMP version not supported"); + + 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; } - timeoutCounter = 0; - updateStatus(ThingStatus.UNKNOWN); refresh = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS); } @@ -230,9 +302,8 @@ public void processPdu(@Nullable CommandResponderEvent event) { 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 void processPdu(@Nullable CommandResponderEvent event) { 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 void processPdu(@Nullable CommandResponderEvent event) { 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 void processPdu(@Nullable CommandResponderEvent event) { 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 channelConfigs = Collections - .unmodifiableSet(thing.getChannels().stream().map(channel -> getChannelConfigFromChannel(channel)) - .filter(Objects::nonNull).collect(Collectors.toSet())); + Set 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 @@ private void updateChannels(OID oid, Variable value, Set unit = channelConfig.unit; if (channelConfig.datatype == SnmpDatatype.FLOAT) { - numericState = new BigDecimal(value.toString()); - } else { - numericState = BigDecimal.valueOf(value.toLong()); - } - if (unit != null) { - state = new QuantityType<>(numericState, unit); + 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 (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 { + 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 { - state = new DecimalType(numericState); + 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 @@ private void updateChannels(OID oid, Variable value, Set { 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 @@ private Variable convertDatatype(Command command, SnmpDatatype datatype) { 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 @@ private void refresh() { 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 @@ private void refresh() { } } } + + private PDU getPDU() { + if (config.protocol == SnmpProtocolVersion.v3 || config.protocol == SnmpProtocolVersion.V3) { + return new ScopedPDU(); + } else { + return new PDU(); + } + } } 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 19c4499939128..5212936a1b304 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 @@ -14,8 +14,8 @@ 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; } 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 1bb61bccc36fa..16440d6b8b866 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 @@ -16,8 +16,8 @@ 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 SnmpInternalChannelConfiguration(ChannelUID channelUID, OID oid, SnmpChan this.onValue = onValue; this.offValue = offValue; this.exceptionValue = exceptionValue; - this.doNotLogException = doNotLogException; this.unit = unit; + this.doNotLogException = doNotLogException; } } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpTargetConfiguration.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpTargetConfiguration.java index 832bbaf486f21..41aa8569810dd 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpTargetConfiguration.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpTargetConfiguration.java @@ -14,7 +14,10 @@ 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 @@ */ @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; } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpAuthProtocol.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpAuthProtocol.java new file mode 100644 index 0000000000000..917e5ef70d9d2 --- /dev/null +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpAuthProtocol.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpChannelMode.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpChannelMode.java similarity index 83% rename from bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpChannelMode.java rename to bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpChannelMode.java index 82a21589f2778..c0273b4152d88 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpChannelMode.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpChannelMode.java @@ -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, diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpDatatype.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpDatatype.java similarity index 84% rename from bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpDatatype.java rename to bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpDatatype.java index 7101407c7ee52..aa953c68333e8 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpDatatype.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpDatatype.java @@ -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, diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpPrivProtocol.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpPrivProtocol.java new file mode 100644 index 0000000000000..c7caecc436677 --- /dev/null +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpPrivProtocol.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpProtocolVersion.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpProtocolVersion.java similarity index 78% rename from bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpProtocolVersion.java rename to bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpProtocolVersion.java index 7700dedf802b9..0cfaf593e9189 100644 --- a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpProtocolVersion.java +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpProtocolVersion.java @@ -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; } diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpSecurityModel.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpSecurityModel.java new file mode 100644 index 0000000000000..c20e84dc69642 --- /dev/null +++ b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpSecurityModel.java @@ -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; + } +} 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 a34b75cb7746e..782d70d6fe065 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 @@ -52,7 +52,92 @@ true + + + + + + + + + Hostname or IP address of target host + network-address + + + + The authorization engine ID of this target in hexadecimal notation (22-64 characters) + + + + + + + + + + + + + true + NO_AUTH_NO_PRIV + + + + + + + + + + + + true + MD5 + + + + password + + + + + + + + + + true + DES + + + + password + + + + Refresh time in s (default 60s) + 60 + + + + + 161 + true + + + + Timeout in ms for a single update request + 1500 + true + + + + Number of retries for an update request + 2 + true + + @@ -76,6 +161,10 @@ READ true + + + The unit of this value. + Content data type @@ -99,12 +188,6 @@ 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 e6de64a631887..ac46fd4320309 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 @@ -23,10 +23,14 @@ 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 @@ * * @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 @@ protected VariableBinding handleCommandSwitchChannel(SnmpDatatype datatype, Comm } } - 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 @@ protected void onResponseNumberStringChannel(SnmpChannelMode channelMode, boolea } } - 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 @@ protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode) 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 channelConfig = new HashMap<>(); Map thingConfig = new HashMap<>(); mocks = MockitoAnnotations.openMocks(this); @@ -184,29 +195,27 @@ protected void setup(ChannelTypeUID channelTypeUID, SnmpChannelMode channelMode, 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()); - if (datatype != null) { - channelConfig.put("datatype", datatype.name()); - } - if (onValue != null) { - channelConfig.put("onvalue", onValue); - } - if (offValue != null) { - channelConfig.put("offvalue", offValue); - } - 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); + String itemType = SnmpBindingConstants.CHANNEL_TYPE_UID_NUMBER.equals(channelTypeUID) ? "Number" : "String"; + channelConfig.put("oid", TEST_OID); + channelConfig.put("mode", channelMode.name()); + if (datatype != null) { + channelConfig.put("datatype", datatype.name()); + } + if (onValue != null) { + channelConfig.put("onvalue", onValue); + } + if (offValue != null) { + channelConfig.put("offvalue", offValue); + } + 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); thing = thingBuilder.build(); thingHandler = new SnmpTargetHandler(thing, snmpService); diff --git a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/NumberChannelTest.java b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/NumberChannelTest.java new file mode 100644 index 0000000000000..e9544bf04ade4 --- /dev/null +++ b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/NumberChannelTest.java @@ -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 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> 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); + } +} 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 347b267e6c350..e4b5ac670cb5f 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 @@ -19,12 +19,15 @@ 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 @@ * * @author Jan N. Klug - Initial contribution */ +@NonNullByDefault public class SnmpTargetHandlerTest extends AbstractSnmpTargetHandlerTest { @Test @@ -52,7 +56,7 @@ public void testChannelsProperlyRefreshing() throws IOException { } @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 void testCommandsAreProperlyHandledByNumberChannel() throws IOException { 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 void testNumberChannelsProperlyUpdatingFloatValue() throws IOException { 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 void testCancelingAsyncRequest() { 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; } } diff --git a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/StringChannelTest.java b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/StringChannelTest.java index d8136e2428e7e..4d09dd3c46587 100644 --- a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/StringChannelTest.java +++ b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/StringChannelTest.java @@ -19,7 +19,10 @@ 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 @@ * * @author Jan N. Klug - Initial contribution */ +@NonNullByDefault public class StringChannelTest extends AbstractSnmpTargetHandlerTest { @Test @@ -43,6 +47,12 @@ public void testCommandsAreProperlyHandledByStringChannel() throws IOException { 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 void testCommandsAreProperlyHandledByStringChannel() throws IOException { 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()); diff --git a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SwitchChannelTest.java b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SwitchChannelTest.java index 3227eeda316e4..0ae04d1446978 100644 --- a/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SwitchChannelTest.java +++ b/bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SwitchChannelTest.java @@ -19,7 +19,10 @@ 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 @@ * * @author Jan N. Klug - Initial contribution */ +@NonNullByDefault public class SwitchChannelTest extends AbstractSnmpTargetHandlerTest { @Test @@ -45,11 +49,23 @@ public void testCommandsAreProperlyHandledBySwitchChannel() throws IOException { 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()); diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java index 5dced54d95d61..0403e9c0f562b 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java @@ -275,7 +275,7 @@ private List createChannelGroups(ThingUID thingUID, Stri List newChannelGroups = new ArrayList<>(); for (int i = 0; i < count; i++) { String index = String.valueOf(i); - ChannelGroupDefinition channelGroupDef = thingTypeProvider + ChannelGroupDefinition channelÏGroupDef = thingTypeProvider .createChannelGroupDefinitionWithIndex(channelGroupID, channelGroupTypeID, i); if (!(channelGroupDef == null || channelGroups.contains(channelGroupID + index))) { logger.trace("Adding channel group {}", channelGroupID + index);