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