]> git.basschouten.com Git - openhab-addons.git/blob
74580916f34716416ae9202bb342d7bc6b9a0b2f
[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
14 package org.openhab.binding.ipcamera.internal;
15
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17
18 import java.nio.charset.StandardCharsets;
19 import java.util.ArrayList;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
24 import org.openhab.core.library.types.OnOffType;
25 import org.openhab.core.library.types.StringType;
26 import org.openhab.core.thing.ChannelUID;
27 import org.openhab.core.thing.binding.ThingHandler;
28 import org.openhab.core.types.Command;
29 import org.openhab.core.types.RefreshType;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import io.netty.buffer.ByteBuf;
34 import io.netty.buffer.Unpooled;
35 import io.netty.channel.ChannelDuplexHandler;
36 import io.netty.channel.ChannelHandlerContext;
37 import io.netty.handler.codec.http.DefaultFullHttpRequest;
38 import io.netty.handler.codec.http.FullHttpRequest;
39 import io.netty.handler.codec.http.HttpHeaderNames;
40 import io.netty.handler.codec.http.HttpHeaderValues;
41 import io.netty.handler.codec.http.HttpMethod;
42 import io.netty.handler.codec.http.HttpVersion;
43 import io.netty.util.ReferenceCountUtil;
44
45 /**
46  * The {@link HikvisionHandler} is responsible for handling commands, which are
47  * sent to one of the channels.
48  *
49  * @author Matthew Skinner - Initial contribution
50  */
51
52 @NonNullByDefault
53 public class HikvisionHandler extends ChannelDuplexHandler {
54     private final Logger logger = LoggerFactory.getLogger(getClass());
55     private IpCameraHandler ipCameraHandler;
56     private int nvrChannel;
57     private int lineCount, vmdCount, leftCount, takenCount, faceCount, pirCount, fieldCount;
58
59     public HikvisionHandler(ThingHandler handler, int nvrChannel) {
60         ipCameraHandler = (IpCameraHandler) handler;
61         this.nvrChannel = nvrChannel;
62     }
63
64     // This handles the incoming http replies back from the camera.
65     @Override
66     public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
67         if (msg == null || ctx == null) {
68             return;
69         }
70         String content = "";
71         int debounce = 3;
72         try {
73             content = msg.toString();
74             if (content.isEmpty()) {
75                 return;
76             }
77             logger.trace("HTTP Result back from camera is \t:{}:", content);
78
79             if (content.contains("--boundary")) {// Alarm checking goes in here//
80                 if (content.contains("<EventNotificationAlert version=\"")) {
81                     if (content.contains("hannelID>" + nvrChannel + "</")) {// some camera use c or <dynChannelID>
82                         if (content.contains("<eventType>linedetection</eventType>")) {
83                             ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
84                             lineCount = debounce;
85                         }
86                         if (content.contains("<eventType>fielddetection</eventType>")) {
87                             ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
88                             fieldCount = debounce;
89                         }
90                         if (content.contains("<eventType>VMD</eventType>")) {
91                             ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
92                             vmdCount = debounce;
93                         }
94                         if (content.contains("<eventType>facedetection</eventType>")) {
95                             ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
96                             faceCount = debounce;
97                         }
98                         if (content.contains("<eventType>unattendedBaggage</eventType>")) {
99                             ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
100                             leftCount = debounce;
101                         }
102                         if (content.contains("<eventType>attendedBaggage</eventType>")) {
103                             ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
104                             takenCount = debounce;
105                         }
106                         if (content.contains("<eventType>PIR</eventType>")) {
107                             ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
108                             pirCount = debounce;
109                         }
110                         if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
111                             if (vmdCount > 1) {
112                                 vmdCount = 1;
113                             }
114                             countDown();
115                             countDown();
116                         }
117                     } else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all channels
118                         if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
119                             if (vmdCount > 1) {
120                                 vmdCount = 1;
121                             }
122                             countDown();
123                             countDown();
124                         }
125                     }
126                     countDown();
127                 }
128             } else {
129                 String replyElement = Helper.fetchXML(content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<");
130                 switch (replyElement) {
131                     case "MotionDetection version=":
132                         ipCameraHandler.storeHttpReply(
133                                 "/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection", content);
134                         if (content.contains("<enabled>true</enabled>")) {
135                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
136                         } else if (content.contains("<enabled>false</enabled>")) {
137                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
138                         }
139                         break;
140                     case "IOInputPort version=":
141                         ipCameraHandler.storeHttpReply("/ISAPI/System/IO/inputs/" + nvrChannel, content);
142                         if (content.contains("<enabled>true</enabled>")) {
143                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
144                         } else if (content.contains("<enabled>false</enabled>")) {
145                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
146                         }
147                         if (content.contains("<triggering>low</triggering>")) {
148                             ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
149                         } else if (content.contains("<triggering>high</triggering>")) {
150                             ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.ON);
151                         }
152                         break;
153                     case "LineDetection":
154                         ipCameraHandler.storeHttpReply("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", content);
155                         if (content.contains("<enabled>true</enabled>")) {
156                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
157                         } else if (content.contains("<enabled>false</enabled>")) {
158                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
159                         }
160                         break;
161                     case "TextOverlay version=":
162                         ipCameraHandler.storeHttpReply(
163                                 "/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1", content);
164                         String text = Helper.fetchXML(content, "<enabled>true</enabled>", "<displayText>");
165                         ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(text));
166                         break;
167                     case "AudioDetection version=":
168                         ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
169                                 content);
170                         if (content.contains("<enabled>true</enabled>")) {
171                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
172                         } else if (content.contains("<enabled>false</enabled>")) {
173                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
174                         }
175                         break;
176                     case "IOPortStatus version=":
177                         if (content.contains("<ioState>active</ioState>")) {
178                             ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
179                         } else if (content.contains("<ioState>inactive</ioState>")) {
180                             ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
181                         }
182                         break;
183                     case "FieldDetection version=":
184                         ipCameraHandler.storeHttpReply("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", content);
185                         if (content.contains("<enabled>true</enabled>")) {
186                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.ON);
187                         } else if (content.contains("<enabled>false</enabled>")) {
188                             ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.OFF);
189                         }
190                         break;
191                     case "ResponseStatus version=":
192                         ////////////////// External Alarm Input ///////////////
193                         if (content.contains(
194                                 "<requestURL>/ISAPI/System/IO/inputs/" + nvrChannel + "/status</requestURL>")) {
195                             // Stops checking the external alarm if camera does not have feature.
196                             if (content.contains("<statusString>Invalid Operation</statusString>")) {
197                                 ipCameraHandler.lowPriorityRequests.remove(0);
198                                 ipCameraHandler.logger.debug(
199                                         "Stopping checks for alarm inputs as camera appears to be missing this feature.");
200                             }
201                         }
202                         break;
203                     default:
204                         if (content.contains("<EventNotificationAlert")) {
205                             if (content.contains("hannelID>" + nvrChannel + "</")
206                                     || content.contains("<channelID>0</channelID>")) {// some camera use c or
207                                                                                       // <dynChannelID>
208                                 if (content.contains(
209                                         "<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
210                                     if (vmdCount > 1) {
211                                         vmdCount = 1;
212                                     }
213                                     countDown();
214                                     countDown();
215                                 }
216                                 countDown();
217                             }
218                         } else {
219                             logger.debug("Unhandled reply-{}.", content);
220                         }
221                         break;
222                 }
223             }
224         } finally {
225             ReferenceCountUtil.release(msg);
226         }
227     }
228
229     // This does debouncing of the alarms
230     void countDown() {
231         if (lineCount > 1) {
232             lineCount--;
233         } else if (lineCount == 1) {
234             ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
235             lineCount--;
236         }
237         if (vmdCount > 1) {
238             vmdCount--;
239         } else if (vmdCount == 1) {
240             ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
241             vmdCount--;
242         }
243         if (leftCount > 1) {
244             leftCount--;
245         } else if (leftCount == 1) {
246             ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
247             leftCount--;
248         }
249         if (takenCount > 1) {
250             takenCount--;
251         } else if (takenCount == 1) {
252             ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
253             takenCount--;
254         }
255         if (faceCount > 1) {
256             faceCount--;
257         } else if (faceCount == 1) {
258             ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
259             faceCount--;
260         }
261         if (pirCount > 1) {
262             pirCount--;
263         } else if (pirCount == 1) {
264             ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
265             pirCount--;
266         }
267         if (fieldCount > 1) {
268             fieldCount--;
269         } else if (fieldCount == 1) {
270             ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
271             fieldCount--;
272         }
273         if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
274                 && lineCount == 0) {
275             ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
276         }
277     }
278
279     public void hikSendXml(String httpPutURL, String xml) {
280         logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
281         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
282         request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
283         request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
284         request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
285         ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
286         request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
287         request.content().clear().writeBytes(bbuf);
288         ipCameraHandler.sendHttpPUT(httpPutURL, request);
289     }
290
291     public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
292         ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
293         if (localTracker == null) {
294             ipCameraHandler.sendHttpGET(httpGetPutURL);
295             logger.debug(
296                     "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
297             return;
298         }
299         String body = localTracker.getReply();
300         if (body.isEmpty()) {
301             logger.debug(
302                     "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
303             ipCameraHandler.sendHttpGET(httpGetPutURL);
304         } else {
305             logger.trace("An OLD reply from the camera was:{}", body);
306             if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
307                 body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
308             }
309             int elementIndexStart = body.indexOf("<" + removeElement + ">");
310             int elementIndexEnd = body.indexOf("</" + removeElement + ">");
311             body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
312                     + body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
313             logger.trace("Body for this PUT is going to be:{}", body);
314             localTracker.setReply(body);
315             FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
316                     httpGetPutURL);
317             request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
318             request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
319             request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
320             ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
321             request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
322             request.content().clear().writeBytes(bbuf);
323             ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
324         }
325     }
326
327     // This handles the commands that come from the Openhab event bus.
328     public void handleCommand(ChannelUID channelUID, Command command) {
329         if (command instanceof RefreshType) {
330             switch (channelUID.getId()) {
331                 case CHANNEL_ENABLE_AUDIO_ALARM:
332                     ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
333                     return;
334                 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
335                     ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
336                     return;
337                 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
338                     ipCameraHandler.logger.debug("FieldDetection command");
339                     ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
340                     return;
341                 case CHANNEL_ENABLE_MOTION_ALARM:
342                     ipCameraHandler
343                             .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
344                     return;
345                 case CHANNEL_TEXT_OVERLAY:
346                     ipCameraHandler
347                             .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
348                     return;
349                 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
350                     ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
351                     return;
352                 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
353                     ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
354                     return;
355             }
356             return; // Return as we have handled the refresh command above and don't need to
357                     // continue further.
358         } // end of "REFRESH"
359         switch (channelUID.getId()) {
360             case CHANNEL_TEXT_OVERLAY:
361                 logger.debug("Changing text overlay to {}", command.toString());
362                 if (command.toString().isEmpty()) {
363                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
364                             "enabled", "<enabled>false</enabled>");
365                 } else {
366                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
367                             "displayText", "<displayText>" + command.toString() + "</displayText>");
368                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
369                             "enabled", "<enabled>true</enabled>");
370                 }
371                 return;
372             case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
373                 logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
374                 if (OnOffType.ON.equals(command)) {
375                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
376                 } else {
377                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
378                 }
379                 return;
380             case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
381                 logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
382                 if (OnOffType.OFF.equals(command)) {
383                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
384                             "<triggering>low</triggering>");
385                 } else {
386                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
387                             "<triggering>high</triggering>");
388                 }
389                 return;
390             case CHANNEL_ENABLE_PIR_ALARM:
391                 if (OnOffType.ON.equals(command)) {
392                     hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
393                 } else {
394                     hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
395                 }
396                 return;
397             case CHANNEL_ENABLE_AUDIO_ALARM:
398                 if (OnOffType.ON.equals(command)) {
399                     hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
400                             "<enabled>true</enabled>");
401                 } else {
402                     hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
403                             "<enabled>false</enabled>");
404                 }
405                 return;
406             case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
407                 if (OnOffType.ON.equals(command)) {
408                     hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
409                             "<enabled>true</enabled>");
410                 } else {
411                     hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
412                             "<enabled>false</enabled>");
413                 }
414                 return;
415             case CHANNEL_ENABLE_MOTION_ALARM:
416                 if (OnOffType.ON.equals(command)) {
417                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
418                             "enabled", "<enabled>true</enabled>");
419                 } else {
420                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
421                             "enabled", "<enabled>false</enabled>");
422                 }
423                 return;
424             case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
425                 if (OnOffType.ON.equals(command)) {
426                     hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
427                             "<enabled>true</enabled>");
428                 } else {
429                     hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
430                             "<enabled>false</enabled>");
431                 }
432                 return;
433             case CHANNEL_ACTIVATE_ALARM_OUTPUT:
434                 if (OnOffType.ON.equals(command)) {
435                     hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
436                             "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n    <outputState>high</outputState>\r\n</IOPortData>\r\n");
437                 } else {
438                     hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
439                             "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n    <outputState>low</outputState>\r\n</IOPortData>\r\n");
440                 }
441                 return;
442         }
443     }
444
445     // If a camera does not need to poll a request as often as snapshots, it can be
446     // added here. Binding steps through the list.
447     public ArrayList<String> getLowPriorityRequests() {
448         ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
449         lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + nvrChannel + "/status"); // must stay in element 0.
450         return lowPriorityRequests;
451     }
452 }