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