]> git.basschouten.com Git - openhab-addons.git/commitdiff
[snmp] Upgrades and enhancements (#14330)
authorJ-N-K <github@klug.nrw>
Sat, 4 Feb 2023 14:47:30 +0000 (15:47 +0100)
committerGitHub <noreply@github.com>
Sat, 4 Feb 2023 14:47:30 +0000 (15:47 +0100)
* [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>
26 files changed:
CODEOWNERS
bundles/org.openhab.binding.snmp/README.md
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBindingConstants.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpChannelMode.java [deleted file]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpDatatype.java [deleted file]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpHandlerFactory.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpProtocolVersion.java [deleted file]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpService.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpServiceImpl.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpTargetHandler.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpChannelConfiguration.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpInternalChannelConfiguration.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/config/SnmpTargetConfiguration.java
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpAuthProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpChannelMode.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpDatatype.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpPrivProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpProtocolVersion.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpSecurityModel.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/main/resources/OH-INF/i18n/snmp.properties
bundles/org.openhab.binding.snmp/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/AbstractSnmpTargetHandlerTest.java
bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/NumberChannelTest.java [new file with mode: 0644]
bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SnmpTargetHandlerTest.java
bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/StringChannelTest.java
bundles/org.openhab.binding.snmp/src/test/java/org/openhab/binding/snmp/internal/SwitchChannelTest.java

index 8a82b49aa7d0a1e23ed315a9d1210eccbbd6c844..e1fe2044e900b7dbc63c25b2513967da462c9e13 100644 (file)
 /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
index a795fc7bb97e0fc48edfb4fe098b665dd752e527..e2cd61acb4a699d73912c3ad57a786e6879c0e6b 100644 (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.
-
-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. 
 
+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,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" }
index 399a6ef6b1f4ba2e144d078f99414a582c5bb5cf..74ba891d649791f1e836f1ffb3993ac70bfad25e 100644 (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");
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/SnmpChannelMode.java
deleted file mode 100644 (file)
index 82a2158..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * 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;
-
-/**
- * The {@link SnmpChannelMode} enum defines the mode of SNMP channels
- *
- * @author Jan N. Klug - Initial contribution
- */
-
-public enum SnmpChannelMode {
-    READ,
-    WRITE,
-    READ_WRITE,
-    TRAP
-}
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/SnmpDatatype.java
deleted file mode 100644 (file)
index 7101407..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * 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;
-
-/**
- * The {@link SnmpDatatype} enum defines the datatype of SNMP channels
- *
- * @author Jan N. Klug - Initial contribution
- */
-
-public enum SnmpDatatype {
-    INT32,
-    UINT32,
-    COUNTER64,
-    FLOAT,
-    STRING,
-    HEXSTRING,
-    IPADDRESS
-}
index b28895d58855bb6e01b30abb3436f92d2ae223a6..6ee2a6047680b9395501ad8f98deaf1ea29baf04 100644 (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;
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/SnmpProtocolVersion.java
deleted file mode 100644 (file)
index 7700ded..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * 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;
-
-/**
- * The {@link SnmpProtocolVersion} enum defines the datatype of SNMP channels
- *
- * @author Jan N. Klug - Initial contribution
- */
-
-public enum SnmpProtocolVersion {
-    v1(0),
-    V1(0),
-    v2c(1),
-    V2C(1);
-
-    private final int value;
-
-    private SnmpProtocolVersion(int value) {
-        this.value = value;
-    }
-
-    public int toInteger() {
-        return value;
-    }
-}
index 2f554445ec18c416b1e7ef483bd7cb64a5459830..17e87d18cbb3a53f74a178986769ba1da1c67e22 100644 (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);
 }
index 2aa9b55805e21780cae1ead3f6b7ff7cfbd8a005..4f655a6140f05f8af4be82842f5a3deb9eb4bdae 100644 (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;
+        }
+    }
 }
index 647ca8dfdda9cde629ed3b96a899bc232e3f09f3..6709a817a3144da2eeb016565e07c1d544bb6f18 100644 (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;
+                            }
+                            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 class SnmpTargetHandler extends BaseThingHandler implements ResponseListe
 
         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 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 (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 @@ 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();
+        }
+    }
 }
index 19c4499939128daa33e8248f0726024356056ab8..5212936a1b3043d11bbc111ed5eef4d7279678e5 100644 (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;
 }
index 1bb61bccc36fa42184b2fdde5953c729d0a60552..16440d6b8b86612ee3a00e6a4c593afee9fa7a9e 100644 (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;
     }
 }
index 832bbaf486f210d003f1996661020181325a8a01..41aa8569810dd344a02e9a51fc893a3333cda363 100644 (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;
 }
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 (file)
index 0000000..917e5ef
--- /dev/null
@@ -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/types/SnmpChannelMode.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpChannelMode.java
new file mode 100644 (file)
index 0000000..c0273b4
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * 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;
+
+/**
+ * The {@link SnmpChannelMode} enum defines the mode of SNMP channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum SnmpChannelMode {
+    READ,
+    WRITE,
+    READ_WRITE,
+    TRAP
+}
diff --git a/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpDatatype.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpDatatype.java
new file mode 100644 (file)
index 0000000..aa953c6
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * 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;
+
+/**
+ * The {@link SnmpDatatype} enum defines the datatype of SNMP channels
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public enum SnmpDatatype {
+    INT32,
+    UINT32,
+    COUNTER64,
+    FLOAT,
+    STRING,
+    HEXSTRING,
+    IPADDRESS
+}
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 (file)
index 0000000..c7caecc
--- /dev/null
@@ -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/types/SnmpProtocolVersion.java b/bundles/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/types/SnmpProtocolVersion.java
new file mode 100644 (file)
index 0000000..0cfaf59
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * 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;
+
+/**
+ * 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),
+    v3(3),
+    V3(3);
+
+    private final int value;
+
+    SnmpProtocolVersion(int value) {
+        this.value = value;
+    }
+
+    public int toInteger() {
+        return 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 (file)
index 0000000..c20e84d
--- /dev/null
@@ -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;
+    }
+}
index 736e0a4b3896a64beddddcee7217e6aeb8c3d718..ee531d1c7690de94b75d9e277dcddb5e0afee47f 100644 (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
 
index a34b75cb7746e0670d5e897ab41a5a740a40c3ed..782d70d6fe0659600cde478b0ffcd80d818e7e9b 100644 (file)
                                <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">
                                <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>
                                <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>
 
index e6de64a6318874da758fb658bccd5c4a822012cb..ac46fd4320309d36cefdf6efce719c1b76d04eac 100644 (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,29 +195,27 @@ 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());
-            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 (file)
index 0000000..e9544bf
--- /dev/null
@@ -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);
+    }
+}
index 347b267e6c350cc071a5d4d09fd0128af93b2199..e4b5ac670cb5f1fad1c822b6750792371af03d77 100644 (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;
         }
     }
index d8136e2428e7ef28db68c3592daff918c711ae85..4d09dd3c465876f0b1fa660c0e78b0ed39b15ee7 100644 (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());
index 3227eeda316e41166d55fee5818f6a76b08db330..0ae04d1446978c9ea491c1a4998f209d01594df5 100644 (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());