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