2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.snmp.internal;
15 import static org.openhab.binding.snmp.internal.SnmpBindingConstants.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.net.InetAddress;
20 import java.net.UnknownHostException;
21 import java.util.Collections;
22 import java.util.Objects;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
30 import javax.measure.Unit;
31 import javax.measure.format.MeasurementParseException;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.snmp.internal.config.SnmpChannelConfiguration;
36 import org.openhab.binding.snmp.internal.config.SnmpInternalChannelConfiguration;
37 import org.openhab.binding.snmp.internal.config.SnmpTargetConfiguration;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.util.ThingHandlerHelper;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.openhab.core.types.util.UnitUtils;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56 import org.snmp4j.AbstractTarget;
57 import org.snmp4j.CommandResponder;
58 import org.snmp4j.CommandResponderEvent;
59 import org.snmp4j.CommunityTarget;
60 import org.snmp4j.PDU;
61 import org.snmp4j.PDUv1;
62 import org.snmp4j.Snmp;
63 import org.snmp4j.event.ResponseEvent;
64 import org.snmp4j.event.ResponseListener;
65 import org.snmp4j.mp.SnmpConstants;
66 import org.snmp4j.smi.Counter64;
67 import org.snmp4j.smi.Integer32;
68 import org.snmp4j.smi.IpAddress;
69 import org.snmp4j.smi.OID;
70 import org.snmp4j.smi.OctetString;
71 import org.snmp4j.smi.UdpAddress;
72 import org.snmp4j.smi.UnsignedInteger32;
73 import org.snmp4j.smi.Variable;
74 import org.snmp4j.smi.VariableBinding;
77 * The {@link SnmpTargetHandler} is responsible for handling commands, which are
78 * sent to one of the channels or update remote channels
80 * @author Jan N. Klug - Initial contribution
83 public class SnmpTargetHandler extends BaseThingHandler implements ResponseListener, CommandResponder {
84 private static final Pattern HEXSTRING_VALIDITY = Pattern.compile("([a-f0-9]{2}[ :-]?)+");
85 private static final Pattern HEXSTRING_EXTRACTOR = Pattern.compile("[^a-f0-9]");
87 private final Logger logger = LoggerFactory.getLogger(SnmpTargetHandler.class);
89 private @NonNullByDefault({}) SnmpTargetConfiguration config;
90 private final SnmpService snmpService;
91 private @Nullable ScheduledFuture<?> refresh;
92 private int timeoutCounter = 0;
94 private @NonNullByDefault({}) AbstractTarget target;
95 private @NonNullByDefault({}) String targetAddressString;
97 private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> readChannelSet;
98 private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> writeChannelSet;
99 private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> trapChannelSet;
101 public SnmpTargetHandler(Thing thing, SnmpService snmpService) {
103 this.snmpService = snmpService;
107 public void handleCommand(ChannelUID channelUID, Command command) {
108 if (target.getAddress() == null && !renewTargetAddress()) {
109 logger.info("failed to renew target address, can't process '{}' to '{}'.", command, channelUID);
114 if (command instanceof RefreshType) {
115 SnmpInternalChannelConfiguration channel = readChannelSet.stream()
116 .filter(c -> channelUID.equals(c.channelUID)).findFirst()
117 .orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
118 PDU pdu = new PDU(PDU.GET, Collections.singletonList(new VariableBinding(channel.oid)));
119 snmpService.send(pdu, target, null, this);
120 } else if (command instanceof DecimalType || command instanceof StringType
121 || command instanceof OnOffType) {
122 SnmpInternalChannelConfiguration channel = writeChannelSet.stream()
123 .filter(config -> channelUID.equals(config.channelUID)).findFirst()
124 .orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
126 if (command instanceof OnOffType) {
127 variable = OnOffType.ON.equals(command) ? channel.onValue : channel.offValue;
128 if (variable == null) {
129 logger.debug("skipping {} to {}: no value defined", command, channelUID);
133 variable = convertDatatype(command, channel.datatype);
135 PDU pdu = new PDU(PDU.SET, Collections.singletonList(new VariableBinding(channel.oid, variable)));
136 snmpService.send(pdu, target, null, this);
138 } catch (IllegalArgumentException e) {
139 logger.warn("can't process command {} to {}: {}", command, channelUID, e.getMessage());
140 } catch (IOException e) {
141 logger.warn("Could not send PDU while processing command {} to {}", command, channelUID);
146 public void initialize() {
147 config = getConfigAs(SnmpTargetConfiguration.class);
149 generateChannelConfigs();
151 if (config.protocol.toInteger() == SnmpConstants.version1
152 || config.protocol.toInteger() == SnmpConstants.version2c) {
153 CommunityTarget target = new CommunityTarget();
154 target.setCommunity(new OctetString(config.community));
155 target.setRetries(config.retries);
156 target.setTimeout(config.timeout);
157 target.setVersion(config.protocol.toInteger());
158 target.setAddress(null);
159 this.target = target;
160 snmpService.addCommandResponder(this);
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SNMP version not supported");
168 updateStatus(ThingStatus.UNKNOWN);
169 refresh = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS);
173 public void dispose() {
174 final ScheduledFuture<?> r = refresh;
175 if (r != null && !r.isCancelled()) {
178 snmpService.removeCommandResponder(this);
182 public void onResponse(@Nullable ResponseEvent event) {
187 if (event.getSource() instanceof Snmp) {
188 // Always cancel async request when response has been received
189 // otherwise a memory leak is created! Not canceling a request
190 // immediately can be useful when sending a request to a broadcast
191 // address (Comment is taken from the snmp4j API doc).
192 ((Snmp) event.getSource()).cancel(event.getRequest(), this);
195 PDU response = event.getResponse();
196 if (response == null) {
197 Exception e = event.getError();
198 if (e == null) { // no response, no error -> request timed out
200 if (timeoutCounter > config.retries) {
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "request timed out");
202 target.setAddress(null);
206 logger.warn("{} requested {} and got error: {}", thing.getUID(), event.getRequest(), e.getMessage());
210 if (ThingHandlerHelper.isHandlerInitialized(this)) {
211 updateStatus(ThingStatus.ONLINE);
213 logger.trace("{} received {}", thing.getUID(), response);
215 response.getVariableBindings().forEach(variable -> {
216 if (variable != null) {
217 updateChannels(variable.getOid(), variable.getVariable(), readChannelSet);
223 public void processPdu(@Nullable CommandResponderEvent event) {
227 logger.trace("{} received trap {}", thing.getUID(), event);
229 final PDU pdu = event.getPDU();
230 final String address = ((UdpAddress) event.getPeerAddress()).getInetAddress().getHostAddress();
231 final String community = new String(event.getSecurityName());
233 if ((pdu.getType() == PDU.V1TRAP) && config.community.equals(community) && (pdu instanceof PDUv1)) {
234 logger.trace("{} received trap is PDUv1.", thing.getUID());
235 PDUv1 pduv1 = (PDUv1) pdu;
236 OID oidEnterprise = pduv1.getEnterprise();
237 int trapValue = pduv1.getGenericTrap();
238 if (trapValue == PDUv1.ENTERPRISE_SPECIFIC) {
239 trapValue = pduv1.getSpecificTrap();
241 updateChannels(oidEnterprise, new UnsignedInteger32(trapValue), trapChannelSet);
243 if ((pdu.getType() == PDU.TRAP || pdu.getType() == PDU.V1TRAP) && config.community.equals(community)
244 && targetAddressString.equals(address)) {
245 pdu.getVariableBindings().forEach(variable -> {
246 if (variable != null) {
247 updateChannels(variable.getOid(), variable.getVariable(), trapChannelSet);
253 private @Nullable SnmpInternalChannelConfiguration getChannelConfigFromChannel(Channel channel) {
254 SnmpChannelConfiguration config = channel.getConfiguration().as(SnmpChannelConfiguration.class);
256 String oid = config.oid;
258 logger.warn("oid must not be null");
262 SnmpDatatype datatype = config.datatype; // maybe null, override later
263 Variable onValue = null;
264 Variable offValue = null;
265 State exceptionValue = UnDefType.UNDEF;
268 if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
269 if (datatype == null) {
270 datatype = SnmpDatatype.INT32;
271 } else if (datatype == SnmpDatatype.IPADDRESS || datatype == SnmpDatatype.STRING) {
274 String configExceptionValue = config.exceptionValue;
275 if (configExceptionValue != null) {
276 exceptionValue = DecimalType.valueOf(configExceptionValue);
278 if (config.unit != null) {
279 if (config.mode != SnmpChannelMode.READ) {
280 logger.warn("units only supported for readonly channels, ignored for channel {}", channel.getUID());
283 unit = UnitUtils.parseUnit(config.unit);
284 } catch (MeasurementParseException e) {
285 logger.warn("unrecognised unit '{}', ignored for channel '{}'", config.unit, channel.getUID());
289 } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
290 if (datatype == null) {
291 datatype = SnmpDatatype.STRING;
292 } else if (datatype != SnmpDatatype.IPADDRESS && datatype != SnmpDatatype.STRING
293 && datatype != SnmpDatatype.HEXSTRING) {
296 String configExceptionValue = config.exceptionValue;
297 if (configExceptionValue != null) {
298 exceptionValue = StringType.valueOf(configExceptionValue);
300 } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
301 if (datatype == null) {
302 datatype = SnmpDatatype.UINT32;
305 final String configOnValue = config.onvalue;
306 if (configOnValue != null) {
307 onValue = convertDatatype(new StringType(configOnValue), datatype);
309 final String configOffValue = config.offvalue;
310 if (configOffValue != null) {
311 offValue = convertDatatype(new StringType(configOffValue), datatype);
313 } catch (IllegalArgumentException e) {
314 logger.warn("illegal value configuration for channel {}", channel.getUID());
317 String configExceptionValue = config.exceptionValue;
318 if (configExceptionValue != null) {
319 exceptionValue = OnOffType.from(configExceptionValue);
322 logger.warn("unknown channel type found for channel {}", channel.getUID());
325 return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(oid), config.mode, datatype, onValue,
326 offValue, exceptionValue, config.doNotLogException, unit);
329 private void generateChannelConfigs() {
330 Set<SnmpInternalChannelConfiguration> channelConfigs = Collections
331 .unmodifiableSet(thing.getChannels().stream().map(channel -> getChannelConfigFromChannel(channel))
332 .filter(Objects::nonNull).collect(Collectors.toSet()));
333 this.readChannelSet = channelConfigs.stream()
334 .filter(c -> c.mode == SnmpChannelMode.READ || c.mode == SnmpChannelMode.READ_WRITE)
335 .collect(Collectors.toSet());
336 this.writeChannelSet = channelConfigs.stream()
337 .filter(c -> c.mode == SnmpChannelMode.WRITE || c.mode == SnmpChannelMode.READ_WRITE)
338 .collect(Collectors.toSet());
339 this.trapChannelSet = channelConfigs.stream().filter(c -> c.mode == SnmpChannelMode.TRAP)
340 .collect(Collectors.toSet());
343 private void updateChannels(OID oid, Variable value, Set<SnmpInternalChannelConfiguration> channelConfigs) {
344 Set<SnmpInternalChannelConfiguration> updateChannelConfigs = channelConfigs.stream()
345 .filter(c -> c.oid.equals(oid)).collect(Collectors.toSet());
346 if (!updateChannelConfigs.isEmpty()) {
347 updateChannelConfigs.forEach(channelConfig -> {
348 ChannelUID channelUID = channelConfig.channelUID;
349 final Channel channel = thing.getChannel(channelUID);
351 if (channel == null) {
352 logger.warn("channel uid {} in channel config set but channel not found", channelUID);
355 if (value.isException()) {
356 if (!channelConfig.doNotLogException) {
357 logger.info("SNMP Exception: request {} returned '{}'", oid, value);
359 state = channelConfig.exceptionValue;
360 } else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
362 BigDecimal numericState;
363 final @Nullable Unit<?> unit = channelConfig.unit;
364 if (channelConfig.datatype == SnmpDatatype.FLOAT) {
365 numericState = new BigDecimal(value.toString());
367 numericState = BigDecimal.valueOf(value.toLong());
370 state = new QuantityType<>(numericState, unit);
372 state = new DecimalType(numericState);
374 } catch (UnsupportedOperationException e) {
375 logger.warn("could not convert {} to number for channel {}", value, channelUID);
378 } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
379 if (channelConfig.datatype == SnmpDatatype.HEXSTRING) {
380 String rawString = ((OctetString) value).toHexString(' ');
381 state = new StringType(rawString.toLowerCase());
383 state = new StringType(value.toString());
385 } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
386 if (value.equals(channelConfig.onValue)) {
387 state = OnOffType.ON;
388 } else if (value.equals(channelConfig.offValue)) {
389 state = OnOffType.OFF;
391 logger.debug("channel {} received unmapped value {} ", channelUID, value);
395 logger.warn("channel {} has unknown ChannelTypeUID", channelUID);
398 updateState(channelUID, state);
401 logger.debug("received value {} for unknown OID {}, skipping", value, oid);
405 private Variable convertDatatype(Command command, SnmpDatatype datatype) {
408 if (command instanceof DecimalType) {
409 return new Integer32(((DecimalType) command).intValue());
410 } else if (command instanceof StringType) {
411 return new Integer32((new DecimalType(((StringType) command).toString())).intValue());
415 if (command instanceof DecimalType) {
416 return new UnsignedInteger32(((DecimalType) command).intValue());
417 } else if (command instanceof StringType) {
418 return new UnsignedInteger32((new DecimalType(((StringType) command).toString())).intValue());
422 if (command instanceof DecimalType) {
423 return new Counter64(((DecimalType) command).longValue());
424 } else if (command instanceof StringType) {
425 return new Counter64((new DecimalType(((StringType) command).toString())).longValue());
430 if (command instanceof DecimalType) {
431 return new OctetString(((DecimalType) command).toString());
432 } else if (command instanceof StringType) {
433 return new OctetString(((StringType) command).toString());
437 if (command instanceof StringType) {
438 String commandString = ((StringType) command).toString().toLowerCase();
439 Matcher commandMatcher = HEXSTRING_VALIDITY.matcher(commandString);
440 if (commandMatcher.matches()) {
441 commandString = HEXSTRING_EXTRACTOR.matcher(commandString).replaceAll("");
442 return OctetString.fromHexStringPairs(commandString);
447 if (command instanceof StringType) {
448 return new IpAddress(((StringType) command).toString());
453 throw new IllegalArgumentException("illegal conversion of " + command + " to " + datatype);
456 private boolean renewTargetAddress() {
458 target.setAddress(new UdpAddress(InetAddress.getByName(config.hostname), config.port));
459 targetAddressString = ((UdpAddress) target.getAddress()).getInetAddress().getHostAddress();
461 } catch (UnknownHostException e) {
462 target.setAddress(null);
463 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot resolve target host");
468 private void refresh() {
469 if (target.getAddress() == null) {
470 if (!renewTargetAddress()) {
471 logger.info("failed to renew target address, waiting for next refresh cycle");
475 PDU pdu = new PDU(PDU.GET,
476 readChannelSet.stream().map(c -> new VariableBinding(c.oid)).collect(Collectors.toList()));
477 if (!pdu.getVariableBindings().isEmpty()) {
479 snmpService.send(pdu, target, null, this);
480 } catch (IOException e) {
481 logger.info("Could not send PDU", e);