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