2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
14 package org.openhab.binding.ipcamera.internal;
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
18 import java.nio.charset.StandardCharsets;
19 import java.util.ArrayList;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
24 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
25 import org.openhab.core.library.types.DecimalType;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.library.types.StringType;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.binding.ThingHandler;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.RefreshType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import io.netty.buffer.ByteBuf;
36 import io.netty.buffer.Unpooled;
37 import io.netty.channel.ChannelDuplexHandler;
38 import io.netty.channel.ChannelHandlerContext;
39 import io.netty.handler.codec.http.DefaultFullHttpRequest;
40 import io.netty.handler.codec.http.FullHttpRequest;
41 import io.netty.handler.codec.http.HttpHeaderNames;
42 import io.netty.handler.codec.http.HttpHeaderValues;
43 import io.netty.handler.codec.http.HttpMethod;
44 import io.netty.handler.codec.http.HttpVersion;
45 import io.netty.util.ReferenceCountUtil;
48 * The {@link HikvisionHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Matthew Skinner - Initial contribution
55 public class HikvisionHandler extends ChannelDuplexHandler {
56 private final Logger logger = LoggerFactory.getLogger(getClass());
57 private IpCameraHandler ipCameraHandler;
58 private int nvrChannel;
59 private int lineCount, vmdCount, leftCount, takenCount, faceCount, pirCount, fieldCount;
61 public HikvisionHandler(ThingHandler handler, int nvrChannel) {
62 ipCameraHandler = (IpCameraHandler) handler;
63 this.nvrChannel = nvrChannel;
66 // This handles the incoming http replies back from the camera.
68 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
69 if (msg == null || ctx == null) {
75 content = msg.toString();
76 if (content.isEmpty()) {
79 logger.trace("HTTP Result back from camera is \t:{}:", content);
81 if (content.contains("--boundary")) {// Alarm checking goes in here//
82 if (content.contains("<EventNotificationAlert version=\"")) {
83 if (content.contains("hannelID>" + nvrChannel + "</")) {// some camera use c or <dynChannelID>
84 if (content.contains("<eventType>linedetection</eventType>")) {
85 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
88 if (content.contains("<eventType>fielddetection</eventType>")) {
89 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
90 fieldCount = debounce;
92 if (content.contains("<eventType>VMD</eventType>")) {
93 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
96 if (content.contains("<eventType>facedetection</eventType>")) {
97 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
100 if (content.contains("<eventType>unattendedBaggage</eventType>")) {
101 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
102 leftCount = debounce;
104 if (content.contains("<eventType>attendedBaggage</eventType>")) {
105 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
106 takenCount = debounce;
108 if (content.contains("<eventType>PIR</eventType>")) {
109 ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
112 if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
119 } else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all channels
120 if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
131 String replyElement = Helper.fetchXML(content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<");
132 switch (replyElement) {
133 case "MotionDetection version=":
134 ipCameraHandler.storeHttpReply(
135 "/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection", content);
136 if (content.contains("<enabled>true</enabled>")) {
137 ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
138 } else if (content.contains("<enabled>false</enabled>")) {
139 ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
142 case "IOInputPort version=":
143 ipCameraHandler.storeHttpReply("/ISAPI/System/IO/inputs/" + nvrChannel, content);
144 if (content.contains("<enabled>true</enabled>")) {
145 ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
146 } else if (content.contains("<enabled>false</enabled>")) {
147 ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
149 if (content.contains("<triggering>low</triggering>")) {
150 ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
151 } else if (content.contains("<triggering>high</triggering>")) {
152 ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.ON);
155 case "LineDetection":
156 ipCameraHandler.storeHttpReply("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", content);
157 if (content.contains("<enabled>true</enabled>")) {
158 ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
159 } else if (content.contains("<enabled>false</enabled>")) {
160 ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
163 case "TextOverlay version=":
164 ipCameraHandler.storeHttpReply(
165 "/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1", content);
166 String text = Helper.fetchXML(content, "<enabled>true</enabled>", "<displayText>");
167 ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(text));
169 case "AudioDetection version=":
170 ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
172 if (content.contains("<enabled>true</enabled>")) {
173 ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
174 } else if (content.contains("<enabled>false</enabled>")) {
175 ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
178 case "IOPortStatus version=":
179 if (content.contains("<ioState>active</ioState>")) {
180 ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
181 } else if (content.contains("<ioState>inactive</ioState>")) {
182 ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
185 case "FieldDetection version=":
186 ipCameraHandler.storeHttpReply("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", content);
187 if (content.contains("<enabled>true</enabled>")) {
188 ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.ON);
189 } else if (content.contains("<enabled>false</enabled>")) {
190 ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.OFF);
193 case "ResponseStatus version=":
194 ////////////////// External Alarm Input ///////////////
195 if (content.contains(
196 "<requestURL>/ISAPI/System/IO/inputs/" + nvrChannel + "/status</requestURL>")) {
197 // Stops checking the external alarm if camera does not have feature.
198 if (content.contains("<statusString>Invalid Operation</statusString>")) {
199 ipCameraHandler.lowPriorityRequests.remove(0);
200 ipCameraHandler.logger.debug(
201 "Stopping checks for alarm inputs as camera appears to be missing this feature.");
206 if (content.contains("<EventNotificationAlert")) {
207 if (content.contains("hannelID>" + nvrChannel + "</")
208 || content.contains("<channelID>0</channelID>")) {// some camera use c or
210 if (content.contains(
211 "<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
221 logger.debug("Unhandled reply-{}.", content);
227 ReferenceCountUtil.release(msg);
231 // This does debouncing of the alarms
236 } else if (lineCount == 1) {
237 ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
242 } else if (vmdCount == 1) {
243 ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
248 } else if (leftCount == 1) {
249 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
252 if (takenCount > 1) {
254 } else if (takenCount == 1) {
255 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
260 } else if (faceCount == 1) {
261 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
266 } else if (pirCount == 1) {
267 ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
270 if (fieldCount > 1) {
272 } else if (fieldCount == 1) {
273 ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
276 if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
278 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
282 public void hikSendXml(String httpPutURL, String xml) {
283 logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
284 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
285 request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
286 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
287 request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
288 ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
289 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
290 request.content().clear().writeBytes(bbuf);
291 ipCameraHandler.sendHttpPUT(httpPutURL, request);
294 public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
295 ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
296 if (localTracker == null) {
297 ipCameraHandler.sendHttpGET(httpGetPutURL);
299 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
302 String body = localTracker.getReply();
303 if (body.isEmpty()) {
305 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
306 ipCameraHandler.sendHttpGET(httpGetPutURL);
308 logger.trace("An OLD reply from the camera was:{}", body);
309 if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
310 body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
312 int elementIndexStart = body.indexOf("<" + removeElement + ">");
313 int elementIndexEnd = body.indexOf("</" + removeElement + ">");
314 body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
315 + body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
316 logger.trace("Body for this PUT is going to be:{}", body);
317 localTracker.setReply(body);
318 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
320 request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
321 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
322 request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
323 ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
324 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
325 request.content().clear().writeBytes(bbuf);
326 ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
330 // This handles the commands that come from the Openhab event bus.
331 public void handleCommand(ChannelUID channelUID, Command command) {
332 if (command instanceof RefreshType) {
333 switch (channelUID.getId()) {
334 case CHANNEL_ENABLE_AUDIO_ALARM:
335 ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
337 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
338 ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
340 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
341 ipCameraHandler.logger.debug("FieldDetection command");
342 ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
344 case CHANNEL_ENABLE_MOTION_ALARM:
346 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
348 case CHANNEL_TEXT_OVERLAY:
350 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
352 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
353 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
355 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
356 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
359 return; // Return as we have handled the refresh command above and don't need to
361 } // end of "REFRESH"
362 switch (channelUID.getId()) {
363 case CHANNEL_TEXT_OVERLAY:
364 logger.debug("Changing text overlay to {}", command.toString());
365 if (command.toString().isEmpty()) {
366 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
367 "enabled", "<enabled>false</enabled>");
369 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
370 "displayText", "<displayText>" + command.toString() + "</displayText>");
371 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
372 "enabled", "<enabled>true</enabled>");
375 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
376 logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
377 if (OnOffType.ON.equals(command)) {
378 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
380 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
383 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
384 logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
385 if (OnOffType.OFF.equals(command)) {
386 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
387 "<triggering>low</triggering>");
389 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
390 "<triggering>high</triggering>");
393 case CHANNEL_ENABLE_PIR_ALARM:
394 if (OnOffType.ON.equals(command)) {
395 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
397 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
400 case CHANNEL_ENABLE_AUDIO_ALARM:
401 if (OnOffType.ON.equals(command)) {
402 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
403 "<enabled>true</enabled>");
405 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
406 "<enabled>false</enabled>");
409 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
410 if (OnOffType.ON.equals(command)) {
411 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
412 "<enabled>true</enabled>");
414 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
415 "<enabled>false</enabled>");
418 case CHANNEL_ENABLE_MOTION_ALARM:
419 if (OnOffType.ON.equals(command)) {
420 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
421 "enabled", "<enabled>true</enabled>");
423 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
424 "enabled", "<enabled>false</enabled>");
427 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
428 if (OnOffType.ON.equals(command)) {
429 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
430 "<enabled>true</enabled>");
432 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
433 "<enabled>false</enabled>");
436 case CHANNEL_ACTIVATE_ALARM_OUTPUT:
437 if (OnOffType.ON.equals(command)) {
438 hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
439 "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>high</outputState>\r\n</IOPortData>\r\n");
441 hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
442 "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>low</outputState>\r\n</IOPortData>\r\n");
445 case CHANNEL_FFMPEG_MOTION_CONTROL:
446 if (OnOffType.ON.equals(command)) {
447 ipCameraHandler.motionAlarmEnabled = true;
448 } else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
449 ipCameraHandler.motionAlarmEnabled = false;
450 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
452 ipCameraHandler.motionAlarmEnabled = true;
453 ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
454 ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
456 ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
461 // If a camera does not need to poll a request as often as snapshots, it can be
462 // added here. Binding steps through the list.
463 public ArrayList<String> getLowPriorityRequests() {
464 ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
465 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + nvrChannel + "/status"); // must stay in element 0.
466 return lowPriorityRequests;