]> git.basschouten.com Git - openhab-addons.git/blob
81e32c6a23f443a715ac5c2a6592a27cfaab2882
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.snmp.internal;
14
15 import static org.openhab.binding.snmp.internal.SnmpBindingConstants.*;
16
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;
22 import java.util.Set;
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;
28
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.thing.util.ThingHandlerHelper;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.RefreshType;
46 import org.openhab.core.types.State;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50 import org.snmp4j.AbstractTarget;
51 import org.snmp4j.CommandResponder;
52 import org.snmp4j.CommandResponderEvent;
53 import org.snmp4j.CommunityTarget;
54 import org.snmp4j.PDU;
55 import org.snmp4j.PDUv1;
56 import org.snmp4j.Snmp;
57 import org.snmp4j.event.ResponseEvent;
58 import org.snmp4j.event.ResponseListener;
59 import org.snmp4j.mp.SnmpConstants;
60 import org.snmp4j.smi.Counter64;
61 import org.snmp4j.smi.Integer32;
62 import org.snmp4j.smi.IpAddress;
63 import org.snmp4j.smi.OID;
64 import org.snmp4j.smi.OctetString;
65 import org.snmp4j.smi.UdpAddress;
66 import org.snmp4j.smi.UnsignedInteger32;
67 import org.snmp4j.smi.Variable;
68 import org.snmp4j.smi.VariableBinding;
69
70 /**
71  * The {@link SnmpTargetHandler} is responsible for handling commands, which are
72  * sent to one of the channels or update remote channels
73  *
74  * @author Jan N. Klug - Initial contribution
75  */
76 @NonNullByDefault
77 public class SnmpTargetHandler extends BaseThingHandler implements ResponseListener, CommandResponder {
78     private static final Pattern HEXSTRING_VALIDITY = Pattern.compile("([a-f0-9]{2}[ :-]?)+");
79     private static final Pattern HEXSTRING_EXTRACTOR = Pattern.compile("[^a-f0-9]");
80
81     private final Logger logger = LoggerFactory.getLogger(SnmpTargetHandler.class);
82
83     private @NonNullByDefault({}) SnmpTargetConfiguration config;
84     private final SnmpService snmpService;
85     private @Nullable ScheduledFuture<?> refresh;
86     private int timeoutCounter = 0;
87
88     private @NonNullByDefault({}) AbstractTarget target;
89     private @NonNullByDefault({}) String targetAddressString;
90
91     private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> readChannelSet;
92     private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> writeChannelSet;
93     private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> trapChannelSet;
94
95     public SnmpTargetHandler(Thing thing, SnmpService snmpService) {
96         super(thing);
97         this.snmpService = snmpService;
98     }
99
100     @Override
101     public void handleCommand(ChannelUID channelUID, Command command) {
102         if (target.getAddress() == null && !renewTargetAddress()) {
103             logger.info("failed to renew target address, can't process '{}' to '{}'.", command, channelUID);
104             return;
105         }
106
107         try {
108             if (command instanceof RefreshType) {
109                 SnmpInternalChannelConfiguration channel = readChannelSet.stream()
110                         .filter(c -> channelUID.equals(c.channelUID)).findFirst()
111                         .orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
112                 PDU pdu = new PDU(PDU.GET, Collections.singletonList(new VariableBinding(channel.oid)));
113                 snmpService.send(pdu, target, null, this);
114             } else if (command instanceof DecimalType || command instanceof StringType
115                     || command instanceof OnOffType) {
116                 SnmpInternalChannelConfiguration channel = writeChannelSet.stream()
117                         .filter(config -> channelUID.equals(config.channelUID)).findFirst()
118                         .orElseThrow(() -> new IllegalArgumentException("no writable channel found"));
119                 Variable variable;
120                 if (command instanceof OnOffType) {
121                     variable = OnOffType.ON.equals(command) ? channel.onValue : channel.offValue;
122                     if (variable == null) {
123                         logger.debug("skipping {} to {}: no value defined", command, channelUID);
124                         return;
125                     }
126                 } else {
127                     variable = convertDatatype(command, channel.datatype);
128                 }
129                 PDU pdu = new PDU(PDU.SET, Collections.singletonList(new VariableBinding(channel.oid, variable)));
130                 snmpService.send(pdu, target, null, this);
131             }
132         } catch (IllegalArgumentException e) {
133             logger.warn("can't process command {} to {}: {}", command, channelUID, e.getMessage());
134         } catch (IOException e) {
135             logger.warn("Could not send PDU while processing command {} to {}", command, channelUID);
136         }
137     }
138
139     @Override
140     public void initialize() {
141         config = getConfigAs(SnmpTargetConfiguration.class);
142
143         generateChannelConfigs();
144
145         if (config.protocol.toInteger() == SnmpConstants.version1
146                 || config.protocol.toInteger() == SnmpConstants.version2c) {
147             CommunityTarget target = new CommunityTarget();
148             target.setCommunity(new OctetString(config.community));
149             target.setRetries(config.retries);
150             target.setTimeout(config.timeout);
151             target.setVersion(config.protocol.toInteger());
152             target.setAddress(null);
153             this.target = target;
154             snmpService.addCommandResponder(this);
155         } else {
156             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SNMP version not supported");
157             return;
158         }
159
160         timeoutCounter = 0;
161
162         updateStatus(ThingStatus.UNKNOWN);
163         refresh = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS);
164     }
165
166     @Override
167     public void dispose() {
168         final ScheduledFuture<?> r = refresh;
169         if (r != null && !r.isCancelled()) {
170             r.cancel(true);
171         }
172         snmpService.removeCommandResponder(this);
173     }
174
175     @Override
176     public void onResponse(@Nullable ResponseEvent event) {
177         if (event == null) {
178             return;
179         }
180
181         if (event.getSource() instanceof Snmp) {
182             // Always cancel async request when response has been received
183             // otherwise a memory leak is created! Not canceling a request
184             // immediately can be useful when sending a request to a broadcast
185             // address (Comment is taken from the snmp4j API doc).
186             ((Snmp) event.getSource()).cancel(event.getRequest(), this);
187         }
188
189         PDU response = event.getResponse();
190         if (response == null) {
191             Exception e = event.getError();
192             if (e == null) { // no response, no error -> request timed out
193                 timeoutCounter++;
194                 if (timeoutCounter > config.retries) {
195                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "request timed out");
196                     target.setAddress(null);
197                 }
198                 return;
199             }
200             logger.warn("{} requested {} and got error: {}", thing.getUID(), event.getRequest(), e.getMessage());
201             return;
202         }
203         timeoutCounter = 0;
204         if (ThingHandlerHelper.isHandlerInitialized(this)) {
205             updateStatus(ThingStatus.ONLINE);
206         }
207         logger.trace("{} received {}", thing.getUID(), response);
208
209         response.getVariableBindings().forEach(variable -> {
210             if (variable != null) {
211                 updateChannels(variable.getOid(), variable.getVariable(), readChannelSet);
212             }
213         });
214     }
215
216     @Override
217     public void processPdu(@Nullable CommandResponderEvent event) {
218         if (event == null) {
219             return;
220         }
221         logger.trace("{} received trap {}", thing.getUID(), event);
222
223         final PDU pdu = event.getPDU();
224         final String address = ((UdpAddress) event.getPeerAddress()).getInetAddress().getHostAddress();
225         final String community = new String(event.getSecurityName());
226
227         if ((pdu.getType() == PDU.V1TRAP) && config.community.equals(community) && (pdu instanceof PDUv1)) {
228             logger.trace("{} received trap is PDUv1.", thing.getUID());
229             PDUv1 pduv1 = (PDUv1) pdu;
230             OID oidEnterprise = pduv1.getEnterprise();
231             int trapValue = pduv1.getGenericTrap();
232             if (trapValue == PDUv1.ENTERPRISE_SPECIFIC) {
233                 trapValue = pduv1.getSpecificTrap();
234             }
235             updateChannels(oidEnterprise, new UnsignedInteger32(trapValue), trapChannelSet);
236         }
237         if ((pdu.getType() == PDU.TRAP || pdu.getType() == PDU.V1TRAP) && config.community.equals(community)
238                 && targetAddressString.equals(address)) {
239             pdu.getVariableBindings().forEach(variable -> {
240                 if (variable != null) {
241                     updateChannels(variable.getOid(), variable.getVariable(), trapChannelSet);
242                 }
243             });
244         }
245     }
246
247     private @Nullable SnmpInternalChannelConfiguration getChannelConfigFromChannel(Channel channel) {
248         SnmpChannelConfiguration config = channel.getConfiguration().as(SnmpChannelConfiguration.class);
249
250         String oid = config.oid;
251         if (oid == null) {
252             logger.warn("oid must not be null");
253             return null;
254         }
255
256         SnmpDatatype datatype = config.datatype; // maybe null, override later
257         Variable onValue = null;
258         Variable offValue = null;
259         State exceptionValue = UnDefType.UNDEF;
260
261         if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
262             if (datatype == null) {
263                 datatype = SnmpDatatype.INT32;
264             } else if (datatype == SnmpDatatype.IPADDRESS || datatype == SnmpDatatype.STRING) {
265                 return null;
266             }
267             String configExceptionValue = config.exceptionValue;
268             if (configExceptionValue != null) {
269                 exceptionValue = DecimalType.valueOf(configExceptionValue);
270             }
271         } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
272             if (datatype == null) {
273                 datatype = SnmpDatatype.STRING;
274             } else if (datatype != SnmpDatatype.IPADDRESS && datatype != SnmpDatatype.STRING
275                     && datatype != SnmpDatatype.HEXSTRING) {
276                 return null;
277             }
278             String configExceptionValue = config.exceptionValue;
279             if (configExceptionValue != null) {
280                 exceptionValue = StringType.valueOf(configExceptionValue);
281             }
282         } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
283             if (datatype == null) {
284                 datatype = SnmpDatatype.UINT32;
285             }
286             try {
287                 final String configOnValue = config.onvalue;
288                 if (configOnValue != null) {
289                     onValue = convertDatatype(new StringType(configOnValue), datatype);
290                 }
291                 final String configOffValue = config.offvalue;
292                 if (configOffValue != null) {
293                     offValue = convertDatatype(new StringType(configOffValue), datatype);
294                 }
295             } catch (IllegalArgumentException e) {
296                 logger.warn("illegal value configuration for channel {}", channel.getUID());
297                 return null;
298             }
299             String configExceptionValue = config.exceptionValue;
300             if (configExceptionValue != null) {
301                 exceptionValue = OnOffType.from(configExceptionValue);
302             }
303         } else {
304             logger.warn("unknown channel type found for channel {}", channel.getUID());
305             return null;
306         }
307         return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(oid), config.mode, datatype, onValue,
308                 offValue, exceptionValue, config.doNotLogException);
309     }
310
311     private void generateChannelConfigs() {
312         Set<SnmpInternalChannelConfiguration> channelConfigs = Collections
313                 .unmodifiableSet(thing.getChannels().stream().map(channel -> getChannelConfigFromChannel(channel))
314                         .filter(Objects::nonNull).collect(Collectors.toSet()));
315         this.readChannelSet = channelConfigs.stream()
316                 .filter(c -> c.mode == SnmpChannelMode.READ || c.mode == SnmpChannelMode.READ_WRITE)
317                 .collect(Collectors.toSet());
318         this.writeChannelSet = channelConfigs.stream()
319                 .filter(c -> c.mode == SnmpChannelMode.WRITE || c.mode == SnmpChannelMode.READ_WRITE)
320                 .collect(Collectors.toSet());
321         this.trapChannelSet = channelConfigs.stream().filter(c -> c.mode == SnmpChannelMode.TRAP)
322                 .collect(Collectors.toSet());
323     }
324
325     private void updateChannels(OID oid, Variable value, Set<SnmpInternalChannelConfiguration> channelConfigs) {
326         Set<SnmpInternalChannelConfiguration> updateChannelConfigs = channelConfigs.stream()
327                 .filter(c -> c.oid.equals(oid)).collect(Collectors.toSet());
328         if (!updateChannelConfigs.isEmpty()) {
329             updateChannelConfigs.forEach(channelConfig -> {
330                 ChannelUID channelUID = channelConfig.channelUID;
331                 final Channel channel = thing.getChannel(channelUID);
332                 State state;
333                 if (channel == null) {
334                     logger.warn("channel uid {} in channel config set but channel not found", channelUID);
335                     return;
336                 }
337                 if (value.isException()) {
338                     if (!channelConfig.doNotLogException) {
339                         logger.info("SNMP Exception: request {} returned '{}'", oid, value);
340                     }
341                     state = channelConfig.exceptionValue;
342                 } else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
343                     try {
344                         if (channelConfig.datatype == SnmpDatatype.FLOAT) {
345                             state = new DecimalType(value.toString());
346                         } else {
347                             state = new DecimalType(value.toLong());
348                         }
349                     } catch (UnsupportedOperationException e) {
350                         logger.warn("could not convert {} to number for channel {}", value, channelUID);
351                         return;
352                     }
353                 } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
354                     if (channelConfig.datatype == SnmpDatatype.HEXSTRING) {
355                         String rawString = ((OctetString) value).toHexString(' ');
356                         state = new StringType(rawString.toLowerCase());
357                     } else {
358                         state = new StringType(value.toString());
359                     }
360                 } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
361                     if (value.equals(channelConfig.onValue)) {
362                         state = OnOffType.ON;
363                     } else if (value.equals(channelConfig.offValue)) {
364                         state = OnOffType.OFF;
365                     } else {
366                         logger.debug("channel {} received unmapped value {} ", channelUID, value);
367                         return;
368                     }
369                 } else {
370                     logger.warn("channel {} has unknown ChannelTypeUID", channelUID);
371                     return;
372                 }
373                 updateState(channelUID, state);
374             });
375         } else {
376             logger.debug("received value {} for unknown OID {}, skipping", value, oid);
377         }
378     }
379
380     private Variable convertDatatype(Command command, SnmpDatatype datatype) {
381         switch (datatype) {
382             case INT32:
383                 if (command instanceof DecimalType) {
384                     return new Integer32(((DecimalType) command).intValue());
385                 } else if (command instanceof StringType) {
386                     return new Integer32((new DecimalType(((StringType) command).toString())).intValue());
387                 }
388                 break;
389             case UINT32:
390                 if (command instanceof DecimalType) {
391                     return new UnsignedInteger32(((DecimalType) command).intValue());
392                 } else if (command instanceof StringType) {
393                     return new UnsignedInteger32((new DecimalType(((StringType) command).toString())).intValue());
394                 }
395                 break;
396             case COUNTER64:
397                 if (command instanceof DecimalType) {
398                     return new Counter64(((DecimalType) command).longValue());
399                 } else if (command instanceof StringType) {
400                     return new Counter64((new DecimalType(((StringType) command).toString())).longValue());
401                 }
402                 break;
403             case FLOAT:
404             case STRING:
405                 if (command instanceof DecimalType) {
406                     return new OctetString(((DecimalType) command).toString());
407                 } else if (command instanceof StringType) {
408                     return new OctetString(((StringType) command).toString());
409                 }
410                 break;
411             case HEXSTRING:
412                 if (command instanceof StringType) {
413                     String commandString = ((StringType) command).toString().toLowerCase();
414                     Matcher commandMatcher = HEXSTRING_VALIDITY.matcher(commandString);
415                     if (commandMatcher.matches()) {
416                         commandString = HEXSTRING_EXTRACTOR.matcher(commandString).replaceAll("");
417                         return OctetString.fromHexStringPairs(commandString);
418                     }
419                 }
420                 break;
421             case IPADDRESS:
422                 if (command instanceof StringType) {
423                     return new IpAddress(((StringType) command).toString());
424                 }
425                 break;
426             default:
427         }
428         throw new IllegalArgumentException("illegal conversion of " + command + " to " + datatype);
429     }
430
431     private boolean renewTargetAddress() {
432         try {
433             target.setAddress(new UdpAddress(InetAddress.getByName(config.hostname), config.port));
434             targetAddressString = ((UdpAddress) target.getAddress()).getInetAddress().getHostAddress();
435             return true;
436         } catch (UnknownHostException e) {
437             target.setAddress(null);
438             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot resolve target host");
439             return false;
440         }
441     }
442
443     private void refresh() {
444         if (target.getAddress() == null) {
445             if (!renewTargetAddress()) {
446                 logger.info("failed to renew target address, waiting for next refresh cycle");
447                 return;
448             }
449         }
450         PDU pdu = new PDU(PDU.GET,
451                 readChannelSet.stream().map(c -> new VariableBinding(c.oid)).collect(Collectors.toList()));
452         if (!pdu.getVariableBindings().isEmpty()) {
453             try {
454                 snmpService.send(pdu, target, null, this);
455             } catch (IOException e) {
456                 logger.info("Could not send PDU", e);
457             }
458         }
459     }
460 }