2 * Copyright (c) 2010-2020 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.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.util.Collections;
21 import java.util.Objects;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27 import java.util.stream.Collectors;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.snmp.internal.config.SnmpChannelConfiguration;
32 import org.openhab.binding.snmp.internal.config.SnmpInternalChannelConfiguration;
33 import org.openhab.binding.snmp.internal.config.SnmpTargetConfiguration;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.Channel;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49 import org.snmp4j.AbstractTarget;
50 import org.snmp4j.CommandResponder;
51 import org.snmp4j.CommandResponderEvent;
52 import org.snmp4j.CommunityTarget;
53 import org.snmp4j.PDU;
54 import org.snmp4j.PDUv1;
55 import org.snmp4j.event.ResponseEvent;
56 import org.snmp4j.event.ResponseListener;
57 import org.snmp4j.mp.SnmpConstants;
58 import org.snmp4j.smi.Counter64;
59 import org.snmp4j.smi.Integer32;
60 import org.snmp4j.smi.IpAddress;
61 import org.snmp4j.smi.OID;
62 import org.snmp4j.smi.OctetString;
63 import org.snmp4j.smi.UdpAddress;
64 import org.snmp4j.smi.UnsignedInteger32;
65 import org.snmp4j.smi.Variable;
66 import org.snmp4j.smi.VariableBinding;
69 * The {@link SnmpTargetHandler} is responsible for handling commands, which are
70 * sent to one of the channels or update remote channels
72 * @author Jan N. Klug - Initial contribution
75 public class SnmpTargetHandler extends BaseThingHandler implements ResponseListener, CommandResponder {
76 private static final Pattern HEXSTRING_VALIDITY = Pattern.compile("([a-f0-9]{2}[ :-]?)+");
77 private static final Pattern HEXSTRING_EXTRACTOR = Pattern.compile("[^a-f0-9]");
79 private final Logger logger = LoggerFactory.getLogger(SnmpTargetHandler.class);
81 private @NonNullByDefault({}) SnmpTargetConfiguration config;
82 private final SnmpService snmpService;
83 private @Nullable ScheduledFuture<?> refresh;
84 private int timeoutCounter = 0;
86 private @NonNullByDefault({}) AbstractTarget target;
87 private @NonNullByDefault({}) String targetAddressString;
89 private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> readChannelSet;
90 private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> writeChannelSet;
91 private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> trapChannelSet;
93 public SnmpTargetHandler(Thing thing, SnmpService snmpService) {
95 this.snmpService = snmpService;
99 public void handleCommand(ChannelUID channelUID, Command command) {
100 if (target.getAddress() == null && !renewTargetAddress()) {
101 logger.info("failed to renew target address, can't process '{}' to '{}'.", command, channelUID);
106 if (command instanceof RefreshType) {
107 SnmpInternalChannelConfiguration channel = readChannelSet.stream()
108 .filter(c -> channelUID.equals(c.channelUID)).findFirst()
109 .orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
110 PDU pdu = new PDU(PDU.GET, Collections.singletonList(new VariableBinding(channel.oid)));
111 snmpService.send(pdu, target, null, this);
112 } else if (command instanceof DecimalType || command instanceof StringType
113 || command instanceof OnOffType) {
114 SnmpInternalChannelConfiguration channel = writeChannelSet.stream()
115 .filter(config -> channelUID.equals(config.channelUID)).findFirst()
116 .orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
118 if (command instanceof OnOffType) {
119 variable = OnOffType.ON.equals(command) ? channel.onValue : channel.offValue;
120 if (variable == null) {
121 logger.debug("skipping {} to {}: no value defined", command, channelUID);
125 variable = convertDatatype(command, channel.datatype);
127 PDU pdu = new PDU(PDU.SET, Collections.singletonList(new VariableBinding(channel.oid, variable)));
128 snmpService.send(pdu, target, null, this);
130 } catch (IllegalArgumentException e) {
131 logger.warn("can't process command {} to {}: {}", command, channelUID, e.getMessage());
132 } catch (IOException e) {
133 logger.warn("Could not send PDU while processing command {} to {}", command, channelUID);
138 public void initialize() {
139 config = getConfigAs(SnmpTargetConfiguration.class);
141 generateChannelConfigs();
143 if (config.protocol.toInteger() == SnmpConstants.version1
144 || config.protocol.toInteger() == SnmpConstants.version2c) {
145 CommunityTarget target = new CommunityTarget();
146 target.setCommunity(new OctetString(config.community));
147 target.setRetries(config.retries);
148 target.setTimeout(config.timeout);
149 target.setVersion(config.protocol.toInteger());
150 target.setAddress(null);
151 this.target = target;
152 snmpService.addCommandResponder(this);
154 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SNMP version not supported");
160 updateStatus(ThingStatus.UNKNOWN);
161 refresh = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS);
165 public void dispose() {
166 final ScheduledFuture<?> r = refresh;
167 if (r != null && !r.isCancelled()) {
170 snmpService.removeCommandResponder(this);
174 public void onResponse(@Nullable ResponseEvent event) {
178 PDU response = event.getResponse();
179 if (response == null) {
180 Exception e = event.getError();
181 if (e == null) { // no response, no error -> request timed out
183 if (timeoutCounter > config.retries) {
184 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "request timed out");
185 target.setAddress(null);
189 logger.warn("{} requested {} and got error: {}", thing.getUID(), event.getRequest(), e.getMessage());
193 logger.trace("{} received {}", thing.getUID(), response);
195 response.getVariableBindings().forEach(variable -> {
196 OID oid = variable.getOid();
197 Variable value = variable.getVariable();
198 updateChannels(oid, value, readChannelSet);
203 public void processPdu(@Nullable CommandResponderEvent event) {
207 logger.trace("{} received trap {}", thing.getUID(), event);
209 final PDU pdu = event.getPDU();
210 final String address = ((UdpAddress) event.getPeerAddress()).getInetAddress().getHostAddress();
211 final String community = new String(event.getSecurityName());
213 if ((pdu.getType() == PDU.V1TRAP) && config.community.equals(community) && (pdu instanceof PDUv1)) {
214 logger.trace("{} received trap is PDUv1.", thing.getUID());
215 PDUv1 pduv1 = (PDUv1) pdu;
216 OID oidEnterprise = pduv1.getEnterprise();
217 int trapValue = pduv1.getGenericTrap();
218 if (trapValue == PDUv1.ENTERPRISE_SPECIFIC) {
219 trapValue = pduv1.getSpecificTrap();
221 updateChannels(oidEnterprise, new UnsignedInteger32(trapValue), trapChannelSet);
223 if ((pdu.getType() == PDU.TRAP || pdu.getType() == PDU.V1TRAP) && config.community.equals(community)
224 && targetAddressString.equals(address)) {
225 pdu.getVariableBindings().forEach(variable -> {
226 OID oid = variable.getOid();
227 Variable value = variable.getVariable();
228 updateChannels(oid, value, trapChannelSet);
233 private @Nullable SnmpInternalChannelConfiguration getChannelConfigFromChannel(Channel channel) {
234 SnmpChannelConfiguration config = channel.getConfiguration().as(SnmpChannelConfiguration.class);
236 SnmpDatatype datatype;
237 Variable onValue = null;
238 Variable offValue = null;
239 State exceptionValue = UnDefType.UNDEF;
241 if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
242 if (config.datatype == null) {
243 datatype = SnmpDatatype.INT32;
244 } else if (config.datatype == SnmpDatatype.IPADDRESS || config.datatype == SnmpDatatype.STRING) {
247 datatype = config.datatype;
249 if (config.exceptionValue != null) {
250 exceptionValue = DecimalType.valueOf(config.exceptionValue);
252 } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
253 if (config.datatype == null) {
254 datatype = SnmpDatatype.STRING;
255 } else if (config.datatype != SnmpDatatype.IPADDRESS && config.datatype != SnmpDatatype.STRING
256 && config.datatype != SnmpDatatype.HEXSTRING) {
259 datatype = config.datatype;
261 if (config.exceptionValue != null) {
262 exceptionValue = StringType.valueOf(config.exceptionValue);
264 } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
265 if (config.datatype == null) {
266 datatype = SnmpDatatype.UINT32;
268 datatype = config.datatype;
271 if (config.onvalue != null) {
272 onValue = convertDatatype(new StringType(config.onvalue), config.datatype);
274 if (config.offvalue != null) {
275 offValue = convertDatatype(new StringType(config.offvalue), config.datatype);
277 } catch (IllegalArgumentException e) {
278 logger.warn("illegal value configuration for channel {}", channel.getUID());
281 if (config.exceptionValue != null) {
282 exceptionValue = OnOffType.from(config.exceptionValue);
285 logger.warn("unknown channel type found for channel {}", channel.getUID());
288 return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(config.oid), config.mode, datatype,
289 onValue, offValue, exceptionValue, config.doNotLogException);
292 private void generateChannelConfigs() {
293 Set<SnmpInternalChannelConfiguration> channelConfigs = Collections
294 .unmodifiableSet(thing.getChannels().stream().map(channel -> getChannelConfigFromChannel(channel))
295 .filter(Objects::nonNull).collect(Collectors.toSet()));
296 this.readChannelSet = channelConfigs.stream()
297 .filter(c -> c.mode == SnmpChannelMode.READ || c.mode == SnmpChannelMode.READ_WRITE)
298 .collect(Collectors.toSet());
299 this.writeChannelSet = channelConfigs.stream()
300 .filter(c -> c.mode == SnmpChannelMode.WRITE || c.mode == SnmpChannelMode.READ_WRITE)
301 .collect(Collectors.toSet());
302 this.trapChannelSet = channelConfigs.stream().filter(c -> c.mode == SnmpChannelMode.TRAP)
303 .collect(Collectors.toSet());
306 private void updateChannels(OID oid, Variable value, Set<SnmpInternalChannelConfiguration> channelConfigs) {
307 Set<SnmpInternalChannelConfiguration> updateChannelConfigs = channelConfigs.stream()
308 .filter(c -> c.oid.equals(oid)).collect(Collectors.toSet());
309 if (!updateChannelConfigs.isEmpty()) {
310 updateChannelConfigs.forEach(channelConfig -> {
311 ChannelUID channelUID = channelConfig.channelUID;
312 final Channel channel = thing.getChannel(channelUID);
314 if (channel == null) {
315 logger.warn("channel uid {} in channel config set but channel not found", channelUID);
318 if (value.isException()) {
319 if (!channelConfig.doNotLogException) {
320 logger.info("SNMP Exception: request {} returned '{}'", oid, value);
322 state = channelConfig.exceptionValue;
323 } else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
325 if (channelConfig.datatype == SnmpDatatype.FLOAT) {
326 state = new DecimalType(value.toString());
328 state = new DecimalType(value.toLong());
330 } catch (UnsupportedOperationException e) {
331 logger.warn("could not convert {} to number for channel {}", value, channelUID);
334 } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
335 if (channelConfig.datatype == SnmpDatatype.HEXSTRING) {
336 String rawString = ((OctetString) value).toHexString(' ');
337 state = new StringType(rawString.toLowerCase());
339 state = new StringType(value.toString());
341 } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
342 if (value.equals(channelConfig.onValue)) {
343 state = OnOffType.ON;
344 } else if (value.equals(channelConfig.offValue)) {
345 state = OnOffType.OFF;
347 logger.debug("channel {} received unmapped value {} ", channelUID, value);
351 logger.warn("channel {} has unknown ChannelTypeUID", channelUID);
354 updateState(channelUID, state);
357 logger.debug("received value {} for unknown OID {}, skipping", value, oid);
361 private Variable convertDatatype(Command command, SnmpDatatype datatype) {
364 if (command instanceof DecimalType) {
365 return new Integer32(((DecimalType) command).intValue());
366 } else if (command instanceof StringType) {
367 return new Integer32((new DecimalType(((StringType) command).toString())).intValue());
371 if (command instanceof DecimalType) {
372 return new UnsignedInteger32(((DecimalType) command).intValue());
373 } else if (command instanceof StringType) {
374 return new UnsignedInteger32((new DecimalType(((StringType) command).toString())).intValue());
378 if (command instanceof DecimalType) {
379 return new Counter64(((DecimalType) command).longValue());
380 } else if (command instanceof StringType) {
381 return new Counter64((new DecimalType(((StringType) command).toString())).longValue());
386 if (command instanceof DecimalType) {
387 return new OctetString(((DecimalType) command).toString());
388 } else if (command instanceof StringType) {
389 return new OctetString(((StringType) command).toString());
393 if (command instanceof StringType) {
394 String commandString = ((StringType) command).toString().toLowerCase();
395 Matcher commandMatcher = HEXSTRING_VALIDITY.matcher(commandString);
396 if (commandMatcher.matches()) {
397 commandString = HEXSTRING_EXTRACTOR.matcher(commandString).replaceAll("");
398 return OctetString.fromHexStringPairs(commandString);
403 if (command instanceof StringType) {
404 return new IpAddress(((StringType) command).toString());
409 throw new IllegalArgumentException("illegal conversion of " + command + " to " + datatype);
412 private boolean renewTargetAddress() {
414 target.setAddress(new UdpAddress(InetAddress.getByName(config.hostname), config.port));
415 targetAddressString = ((UdpAddress) target.getAddress()).getInetAddress().getHostAddress();
416 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
418 } catch (UnknownHostException e) {
419 target.setAddress(null);
420 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot resolve target host");
425 private void refresh() {
426 if (target.getAddress() == null) {
427 if (!renewTargetAddress()) {
428 logger.info("failed to renew target address, waiting for next refresh cycle");
432 PDU pdu = new PDU(PDU.GET,
433 readChannelSet.stream().map(c -> new VariableBinding(c.oid)).collect(Collectors.toList()));
434 if (!pdu.getVariableBindings().isEmpty()) {
436 snmpService.send(pdu, target, null, this);
437 } catch (IOException e) {
438 logger.info("Could not send PDU", e);