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