]> git.basschouten.com Git - openhab-addons.git/blob
1e35357fff0a82e853b4dc39057f525fa0dac2ce
[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             OID oid = variable.getOid();
211             Variable value = variable.getVariable();
212             updateChannels(oid, value, readChannelSet);
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                 OID oid = variable.getOid();
241                 Variable value = variable.getVariable();
242                 updateChannels(oid, value, trapChannelSet);
243             });
244         }
245     }
246
247     private @Nullable SnmpInternalChannelConfiguration getChannelConfigFromChannel(Channel channel) {
248         SnmpChannelConfiguration config = channel.getConfiguration().as(SnmpChannelConfiguration.class);
249
250         SnmpDatatype datatype;
251         Variable onValue = null;
252         Variable offValue = null;
253         State exceptionValue = UnDefType.UNDEF;
254
255         if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
256             if (config.datatype == null) {
257                 datatype = SnmpDatatype.INT32;
258             } else if (config.datatype == SnmpDatatype.IPADDRESS || config.datatype == SnmpDatatype.STRING) {
259                 return null;
260             } else {
261                 datatype = config.datatype;
262             }
263             if (config.exceptionValue != null) {
264                 exceptionValue = DecimalType.valueOf(config.exceptionValue);
265             }
266         } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
267             if (config.datatype == null) {
268                 datatype = SnmpDatatype.STRING;
269             } else if (config.datatype != SnmpDatatype.IPADDRESS && config.datatype != SnmpDatatype.STRING
270                     && config.datatype != SnmpDatatype.HEXSTRING) {
271                 return null;
272             } else {
273                 datatype = config.datatype;
274             }
275             if (config.exceptionValue != null) {
276                 exceptionValue = StringType.valueOf(config.exceptionValue);
277             }
278         } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
279             if (config.datatype == null) {
280                 datatype = SnmpDatatype.UINT32;
281             } else {
282                 datatype = config.datatype;
283             }
284             try {
285                 if (config.onvalue != null) {
286                     onValue = convertDatatype(new StringType(config.onvalue), config.datatype);
287                 }
288                 if (config.offvalue != null) {
289                     offValue = convertDatatype(new StringType(config.offvalue), config.datatype);
290                 }
291             } catch (IllegalArgumentException e) {
292                 logger.warn("illegal value configuration for channel {}", channel.getUID());
293                 return null;
294             }
295             if (config.exceptionValue != null) {
296                 exceptionValue = OnOffType.from(config.exceptionValue);
297             }
298         } else {
299             logger.warn("unknown channel type found for channel {}", channel.getUID());
300             return null;
301         }
302         return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(config.oid), config.mode, datatype,
303                 onValue, offValue, exceptionValue, config.doNotLogException);
304     }
305
306     private void generateChannelConfigs() {
307         Set<SnmpInternalChannelConfiguration> channelConfigs = Collections
308                 .unmodifiableSet(thing.getChannels().stream().map(channel -> getChannelConfigFromChannel(channel))
309                         .filter(Objects::nonNull).collect(Collectors.toSet()));
310         this.readChannelSet = channelConfigs.stream()
311                 .filter(c -> c.mode == SnmpChannelMode.READ || c.mode == SnmpChannelMode.READ_WRITE)
312                 .collect(Collectors.toSet());
313         this.writeChannelSet = channelConfigs.stream()
314                 .filter(c -> c.mode == SnmpChannelMode.WRITE || c.mode == SnmpChannelMode.READ_WRITE)
315                 .collect(Collectors.toSet());
316         this.trapChannelSet = channelConfigs.stream().filter(c -> c.mode == SnmpChannelMode.TRAP)
317                 .collect(Collectors.toSet());
318     }
319
320     private void updateChannels(OID oid, Variable value, Set<SnmpInternalChannelConfiguration> channelConfigs) {
321         Set<SnmpInternalChannelConfiguration> updateChannelConfigs = channelConfigs.stream()
322                 .filter(c -> c.oid.equals(oid)).collect(Collectors.toSet());
323         if (!updateChannelConfigs.isEmpty()) {
324             updateChannelConfigs.forEach(channelConfig -> {
325                 ChannelUID channelUID = channelConfig.channelUID;
326                 final Channel channel = thing.getChannel(channelUID);
327                 State state;
328                 if (channel == null) {
329                     logger.warn("channel uid {} in channel config set but channel not found", channelUID);
330                     return;
331                 }
332                 if (value.isException()) {
333                     if (!channelConfig.doNotLogException) {
334                         logger.info("SNMP Exception: request {} returned '{}'", oid, value);
335                     }
336                     state = channelConfig.exceptionValue;
337                 } else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
338                     try {
339                         if (channelConfig.datatype == SnmpDatatype.FLOAT) {
340                             state = new DecimalType(value.toString());
341                         } else {
342                             state = new DecimalType(value.toLong());
343                         }
344                     } catch (UnsupportedOperationException e) {
345                         logger.warn("could not convert {} to number for channel {}", value, channelUID);
346                         return;
347                     }
348                 } else if (CHANNEL_TYPE_UID_STRING.equals(channel.getChannelTypeUID())) {
349                     if (channelConfig.datatype == SnmpDatatype.HEXSTRING) {
350                         String rawString = ((OctetString) value).toHexString(' ');
351                         state = new StringType(rawString.toLowerCase());
352                     } else {
353                         state = new StringType(value.toString());
354                     }
355                 } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
356                     if (value.equals(channelConfig.onValue)) {
357                         state = OnOffType.ON;
358                     } else if (value.equals(channelConfig.offValue)) {
359                         state = OnOffType.OFF;
360                     } else {
361                         logger.debug("channel {} received unmapped value {} ", channelUID, value);
362                         return;
363                     }
364                 } else {
365                     logger.warn("channel {} has unknown ChannelTypeUID", channelUID);
366                     return;
367                 }
368                 updateState(channelUID, state);
369             });
370         } else {
371             logger.debug("received value {} for unknown OID {}, skipping", value, oid);
372         }
373     }
374
375     private Variable convertDatatype(Command command, SnmpDatatype datatype) {
376         switch (datatype) {
377             case INT32:
378                 if (command instanceof DecimalType) {
379                     return new Integer32(((DecimalType) command).intValue());
380                 } else if (command instanceof StringType) {
381                     return new Integer32((new DecimalType(((StringType) command).toString())).intValue());
382                 }
383                 break;
384             case UINT32:
385                 if (command instanceof DecimalType) {
386                     return new UnsignedInteger32(((DecimalType) command).intValue());
387                 } else if (command instanceof StringType) {
388                     return new UnsignedInteger32((new DecimalType(((StringType) command).toString())).intValue());
389                 }
390                 break;
391             case COUNTER64:
392                 if (command instanceof DecimalType) {
393                     return new Counter64(((DecimalType) command).longValue());
394                 } else if (command instanceof StringType) {
395                     return new Counter64((new DecimalType(((StringType) command).toString())).longValue());
396                 }
397                 break;
398             case FLOAT:
399             case STRING:
400                 if (command instanceof DecimalType) {
401                     return new OctetString(((DecimalType) command).toString());
402                 } else if (command instanceof StringType) {
403                     return new OctetString(((StringType) command).toString());
404                 }
405                 break;
406             case HEXSTRING:
407                 if (command instanceof StringType) {
408                     String commandString = ((StringType) command).toString().toLowerCase();
409                     Matcher commandMatcher = HEXSTRING_VALIDITY.matcher(commandString);
410                     if (commandMatcher.matches()) {
411                         commandString = HEXSTRING_EXTRACTOR.matcher(commandString).replaceAll("");
412                         return OctetString.fromHexStringPairs(commandString);
413                     }
414                 }
415                 break;
416             case IPADDRESS:
417                 if (command instanceof StringType) {
418                     return new IpAddress(((StringType) command).toString());
419                 }
420                 break;
421             default:
422         }
423         throw new IllegalArgumentException("illegal conversion of " + command + " to " + datatype);
424     }
425
426     private boolean renewTargetAddress() {
427         try {
428             target.setAddress(new UdpAddress(InetAddress.getByName(config.hostname), config.port));
429             targetAddressString = ((UdpAddress) target.getAddress()).getInetAddress().getHostAddress();
430             return true;
431         } catch (UnknownHostException e) {
432             target.setAddress(null);
433             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot resolve target host");
434             return false;
435         }
436     }
437
438     private void refresh() {
439         if (target.getAddress() == null) {
440             if (!renewTargetAddress()) {
441                 logger.info("failed to renew target address, waiting for next refresh cycle");
442                 return;
443             }
444         }
445         PDU pdu = new PDU(PDU.GET,
446                 readChannelSet.stream().map(c -> new VariableBinding(c.oid)).collect(Collectors.toList()));
447         if (!pdu.getVariableBindings().isEmpty()) {
448             try {
449                 snmpService.send(pdu, target, null, this);
450             } catch (IOException e) {
451                 logger.info("Could not send PDU", e);
452             }
453         }
454     }
455 }