]> git.basschouten.com Git - openhab-addons.git/blob
29369774b1431d6d8e32e3eb219f7e8e656bea84
[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.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;
67
68 /**
69  * The {@link SnmpTargetHandler} is responsible for handling commands, which are
70  * sent to one of the channels or update remote channels
71  *
72  * @author Jan N. Klug - Initial contribution
73  */
74 @NonNullByDefault
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]");
78
79     private final Logger logger = LoggerFactory.getLogger(SnmpTargetHandler.class);
80
81     private @NonNullByDefault({}) SnmpTargetConfiguration config;
82     private final SnmpService snmpService;
83     private @Nullable ScheduledFuture<?> refresh;
84     private int timeoutCounter = 0;
85
86     private @NonNullByDefault({}) AbstractTarget target;
87     private @NonNullByDefault({}) String targetAddressString;
88
89     private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> readChannelSet;
90     private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> writeChannelSet;
91     private @NonNullByDefault({}) Set<SnmpInternalChannelConfiguration> trapChannelSet;
92
93     public SnmpTargetHandler(Thing thing, SnmpService snmpService) {
94         super(thing);
95         this.snmpService = snmpService;
96     }
97
98     @Override
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);
102             return;
103         }
104
105         try {
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"));
117                 Variable variable;
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);
122                         return;
123                     }
124                 } else {
125                     variable = convertDatatype(command, channel.datatype);
126                 }
127                 PDU pdu = new PDU(PDU.SET, Collections.singletonList(new VariableBinding(channel.oid, variable)));
128                 snmpService.send(pdu, target, null, this);
129             }
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);
134         }
135     }
136
137     @Override
138     public void initialize() {
139         config = getConfigAs(SnmpTargetConfiguration.class);
140
141         generateChannelConfigs();
142
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);
153         } else {
154             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "SNMP version not supported");
155             return;
156         }
157
158         timeoutCounter = 0;
159
160         updateStatus(ThingStatus.UNKNOWN);
161         refresh = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS);
162     }
163
164     @Override
165     public void dispose() {
166         final ScheduledFuture<?> r = refresh;
167         if (r != null && !r.isCancelled()) {
168             r.cancel(true);
169         }
170         snmpService.removeCommandResponder(this);
171     }
172
173     @Override
174     public void onResponse(@Nullable ResponseEvent event) {
175         if (event == null) {
176             return;
177         }
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
182                 timeoutCounter++;
183                 if (timeoutCounter > config.retries) {
184                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "request timed out");
185                     target.setAddress(null);
186                 }
187                 return;
188             }
189             logger.warn("{} requested {} and got error: {}", thing.getUID(), event.getRequest(), e.getMessage());
190             return;
191         }
192         timeoutCounter = 0;
193         logger.trace("{} received {}", thing.getUID(), response);
194
195         response.getVariableBindings().forEach(variable -> {
196             OID oid = variable.getOid();
197             Variable value = variable.getVariable();
198             updateChannels(oid, value, readChannelSet);
199         });
200     }
201
202     @Override
203     public void processPdu(@Nullable CommandResponderEvent event) {
204         if (event == null) {
205             return;
206         }
207         logger.trace("{} received trap {}", thing.getUID(), event);
208
209         final PDU pdu = event.getPDU();
210         final String address = ((UdpAddress) event.getPeerAddress()).getInetAddress().getHostAddress();
211         final String community = new String(event.getSecurityName());
212
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();
220             }
221             updateChannels(oidEnterprise, new UnsignedInteger32(trapValue), trapChannelSet);
222         }
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);
229             });
230         }
231     }
232
233     private @Nullable SnmpInternalChannelConfiguration getChannelConfigFromChannel(Channel channel) {
234         SnmpChannelConfiguration config = channel.getConfiguration().as(SnmpChannelConfiguration.class);
235
236         SnmpDatatype datatype;
237         Variable onValue = null;
238         Variable offValue = null;
239         State exceptionValue = UnDefType.UNDEF;
240
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) {
245                 return null;
246             } else {
247                 datatype = config.datatype;
248             }
249             if (config.exceptionValue != null) {
250                 exceptionValue = DecimalType.valueOf(config.exceptionValue);
251             }
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) {
257                 return null;
258             } else {
259                 datatype = config.datatype;
260             }
261             if (config.exceptionValue != null) {
262                 exceptionValue = StringType.valueOf(config.exceptionValue);
263             }
264         } else if (CHANNEL_TYPE_UID_SWITCH.equals(channel.getChannelTypeUID())) {
265             if (config.datatype == null) {
266                 datatype = SnmpDatatype.UINT32;
267             } else {
268                 datatype = config.datatype;
269             }
270             try {
271                 if (config.onvalue != null) {
272                     onValue = convertDatatype(new StringType(config.onvalue), config.datatype);
273                 }
274                 if (config.offvalue != null) {
275                     offValue = convertDatatype(new StringType(config.offvalue), config.datatype);
276                 }
277             } catch (IllegalArgumentException e) {
278                 logger.warn("illegal value configuration for channel {}", channel.getUID());
279                 return null;
280             }
281             if (config.exceptionValue != null) {
282                 exceptionValue = OnOffType.from(config.exceptionValue);
283             }
284         } else {
285             logger.warn("unknown channel type found for channel {}", channel.getUID());
286             return null;
287         }
288         return new SnmpInternalChannelConfiguration(channel.getUID(), new OID(config.oid), config.mode, datatype,
289                 onValue, offValue, exceptionValue, config.doNotLogException);
290     }
291
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());
304     }
305
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);
313                 State state;
314                 if (channel == null) {
315                     logger.warn("channel uid {} in channel config set but channel not found", channelUID);
316                     return;
317                 }
318                 if (value.isException()) {
319                     if (!channelConfig.doNotLogException) {
320                         logger.info("SNMP Exception: request {} returned '{}'", oid, value);
321                     }
322                     state = channelConfig.exceptionValue;
323                 } else if (CHANNEL_TYPE_UID_NUMBER.equals(channel.getChannelTypeUID())) {
324                     try {
325                         if (channelConfig.datatype == SnmpDatatype.FLOAT) {
326                             state = new DecimalType(value.toString());
327                         } else {
328                             state = new DecimalType(value.toLong());
329                         }
330                     } catch (UnsupportedOperationException e) {
331                         logger.warn("could not convert {} to number for channel {}", value, channelUID);
332                         return;
333                     }
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());
338                     } else {
339                         state = new StringType(value.toString());
340                     }
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;
346                     } else {
347                         logger.debug("channel {} received unmapped value {} ", channelUID, value);
348                         return;
349                     }
350                 } else {
351                     logger.warn("channel {} has unknown ChannelTypeUID", channelUID);
352                     return;
353                 }
354                 updateState(channelUID, state);
355             });
356         } else {
357             logger.debug("received value {} for unknown OID {}, skipping", value, oid);
358         }
359     }
360
361     private Variable convertDatatype(Command command, SnmpDatatype datatype) {
362         switch (datatype) {
363             case INT32:
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());
368                 }
369                 break;
370             case UINT32:
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());
375                 }
376                 break;
377             case COUNTER64:
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());
382                 }
383                 break;
384             case FLOAT:
385             case STRING:
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());
390                 }
391                 break;
392             case HEXSTRING:
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);
399                     }
400                 }
401                 break;
402             case IPADDRESS:
403                 if (command instanceof StringType) {
404                     return new IpAddress(((StringType) command).toString());
405                 }
406                 break;
407             default:
408         }
409         throw new IllegalArgumentException("illegal conversion of " + command + " to " + datatype);
410     }
411
412     private boolean renewTargetAddress() {
413         try {
414             target.setAddress(new UdpAddress(InetAddress.getByName(config.hostname), config.port));
415             targetAddressString = ((UdpAddress) target.getAddress()).getInetAddress().getHostAddress();
416             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
417             return true;
418         } catch (UnknownHostException e) {
419             target.setAddress(null);
420             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot resolve target host");
421             return false;
422         }
423     }
424
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");
429                 return;
430             }
431         }
432         PDU pdu = new PDU(PDU.GET,
433                 readChannelSet.stream().map(c -> new VariableBinding(c.oid)).collect(Collectors.toList()));
434         if (!pdu.getVariableBindings().isEmpty()) {
435             try {
436                 snmpService.send(pdu, target, null, this);
437             } catch (IOException e) {
438                 logger.info("Could not send PDU", e);
439             }
440         }
441     }
442 }