]> git.basschouten.com Git - openhab-addons.git/blob
a2e86da0677c20a416f90be304a26d02b7046b88
[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     private void processEvent(String content) {
64         // some cameras use <dynChannelID> or <channelID> and NVRs use channel 0 to say all channels
65         if (content.contains("hannelID>" + nvrChannel) || content.contains("<channelID>0</channelID>")) {
66             final int debounce = 3;
67             String eventType = Helper.fetchXML(content, "", "<eventType>");
68             switch (eventType) {
69                 case "videoloss":
70                     if (content.contains("<eventState>inactive</eventState>")) {
71                         if (vmdCount > 1) {
72                             vmdCount = 1;
73                         }
74                         countDown();
75                         countDown();
76                     }
77                     break;
78                 case "PIR":
79                     ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
80                     pirCount = debounce;
81                     break;
82                 case "attendedBaggage":
83                     ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
84                     takenCount = debounce;
85                     break;
86                 case "unattendedBaggage":
87                     ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
88                     leftCount = debounce;
89                     break;
90                 case "facedetection":
91                     ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
92                     faceCount = debounce;
93                     break;
94                 case "VMD":
95                     ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
96                     vmdCount = debounce;
97                     break;
98                 case "fielddetection":
99                     ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
100                     fieldCount = debounce;
101                     break;
102                 case "linedetection":
103                     ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
104                     lineCount = debounce;
105                     break;
106                 default:
107                     logger.debug("Unrecognised Hikvision eventType={}", eventType);
108             }
109         }
110         countDown();
111     }
112
113     // This handles the incoming http replies back from the camera.
114     @Override
115     public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
116         if (msg == null || ctx == null) {
117             return;
118         }
119         try {
120             String content = msg.toString();
121             logger.trace("HTTP Result back from camera is \t:{}:", content);
122             if (content.startsWith("--boundary")) {// Alarm checking goes in here//
123                 processEvent(content);
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                         logger.debug("Unhandled reply-{}.", content);
201                         break;
202                 }
203             }
204         } finally {
205             ReferenceCountUtil.release(msg);
206         }
207     }
208
209     // This does debouncing of the alarms
210     void countDown() {
211         if (lineCount > 1) {
212             lineCount--;
213         } else if (lineCount == 1) {
214             ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
215             lineCount--;
216         }
217         if (vmdCount > 1) {
218             vmdCount--;
219         } else if (vmdCount == 1) {
220             ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
221             vmdCount--;
222         }
223         if (leftCount > 1) {
224             leftCount--;
225         } else if (leftCount == 1) {
226             ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
227             leftCount--;
228         }
229         if (takenCount > 1) {
230             takenCount--;
231         } else if (takenCount == 1) {
232             ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
233             takenCount--;
234         }
235         if (faceCount > 1) {
236             faceCount--;
237         } else if (faceCount == 1) {
238             ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
239             faceCount--;
240         }
241         if (pirCount > 1) {
242             pirCount--;
243         } else if (pirCount == 1) {
244             ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
245             pirCount--;
246         }
247         if (fieldCount > 1) {
248             fieldCount--;
249         } else if (fieldCount == 1) {
250             ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
251             fieldCount--;
252         }
253         if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
254                 && lineCount == 0) {
255             ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
256         }
257     }
258
259     public void hikSendXml(String httpPutURL, String xml) {
260         logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
261         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
262         request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
263         request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
264         request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
265         ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
266         request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
267         request.content().clear().writeBytes(bbuf);
268         ipCameraHandler.sendHttpPUT(httpPutURL, request);
269     }
270
271     public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
272         ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
273         if (localTracker == null) {
274             ipCameraHandler.sendHttpGET(httpGetPutURL);
275             logger.debug(
276                     "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
277             return;
278         }
279         String body = localTracker.getReply();
280         if (body.isEmpty()) {
281             logger.debug(
282                     "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
283             ipCameraHandler.sendHttpGET(httpGetPutURL);
284         } else {
285             logger.trace("An OLD reply from the camera was:{}", body);
286             if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
287                 body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
288             }
289             int elementIndexStart = body.indexOf("<" + removeElement + ">");
290             int elementIndexEnd = body.indexOf("</" + removeElement + ">");
291             body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
292                     + body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
293             logger.trace("Body for this PUT is going to be:{}", body);
294             localTracker.setReply(body);
295             FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
296                     httpGetPutURL);
297             request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
298             request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
299             request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
300             ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
301             request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
302             request.content().clear().writeBytes(bbuf);
303             ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
304         }
305     }
306
307     // This handles the commands that come from the Openhab event bus.
308     public void handleCommand(ChannelUID channelUID, Command command) {
309         if (command instanceof RefreshType) {
310             switch (channelUID.getId()) {
311                 case CHANNEL_ENABLE_AUDIO_ALARM:
312                     ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
313                     return;
314                 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
315                     ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
316                     return;
317                 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
318                     ipCameraHandler.logger.debug("FieldDetection command");
319                     ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
320                     return;
321                 case CHANNEL_ENABLE_MOTION_ALARM:
322                     ipCameraHandler
323                             .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
324                     return;
325                 case CHANNEL_TEXT_OVERLAY:
326                     ipCameraHandler
327                             .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
328                     return;
329                 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
330                     ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
331                     return;
332                 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
333                     ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
334                     return;
335             }
336             return; // Return as we have handled the refresh command above and don't need to
337                     // continue further.
338         } // end of "REFRESH"
339         switch (channelUID.getId()) {
340             case CHANNEL_TEXT_OVERLAY:
341                 logger.debug("Changing text overlay to {}", command.toString());
342                 if (command.toString().isEmpty()) {
343                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
344                             "enabled", "<enabled>false</enabled>");
345                 } else {
346                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
347                             "displayText", "<displayText>" + command.toString() + "</displayText>");
348                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
349                             "enabled", "<enabled>true</enabled>");
350                 }
351                 return;
352             case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
353                 logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
354                 if (OnOffType.ON.equals(command)) {
355                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
356                 } else {
357                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
358                 }
359                 return;
360             case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
361                 logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
362                 if (OnOffType.OFF.equals(command)) {
363                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
364                             "<triggering>low</triggering>");
365                 } else {
366                     hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
367                             "<triggering>high</triggering>");
368                 }
369                 return;
370             case CHANNEL_ENABLE_PIR_ALARM:
371                 if (OnOffType.ON.equals(command)) {
372                     hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
373                 } else {
374                     hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
375                 }
376                 return;
377             case CHANNEL_ENABLE_AUDIO_ALARM:
378                 if (OnOffType.ON.equals(command)) {
379                     hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
380                             "<enabled>true</enabled>");
381                 } else {
382                     hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
383                             "<enabled>false</enabled>");
384                 }
385                 return;
386             case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
387                 if (OnOffType.ON.equals(command)) {
388                     hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
389                             "<enabled>true</enabled>");
390                 } else {
391                     hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
392                             "<enabled>false</enabled>");
393                 }
394                 return;
395             case CHANNEL_ENABLE_MOTION_ALARM:
396                 if (OnOffType.ON.equals(command)) {
397                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
398                             "enabled", "<enabled>true</enabled>");
399                 } else {
400                     hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
401                             "enabled", "<enabled>false</enabled>");
402                 }
403                 return;
404             case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
405                 if (OnOffType.ON.equals(command)) {
406                     hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
407                             "<enabled>true</enabled>");
408                 } else {
409                     hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
410                             "<enabled>false</enabled>");
411                 }
412                 return;
413             case CHANNEL_ACTIVATE_ALARM_OUTPUT:
414                 if (OnOffType.ON.equals(command)) {
415                     hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
416                             "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n    <outputState>high</outputState>\r\n</IOPortData>\r\n");
417                 } else {
418                     hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
419                             "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n    <outputState>low</outputState>\r\n</IOPortData>\r\n");
420                 }
421                 return;
422         }
423     }
424
425     // If a camera does not need to poll a request as often as snapshots, it can be
426     // added here. Binding steps through the list.
427     public ArrayList<String> getLowPriorityRequests() {
428         ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
429         lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + nvrChannel + "/status"); // must stay in element 0.
430         return lowPriorityRequests;
431     }
432 }