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