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.handler.IpCameraHandler;
24 import org.openhab.core.library.types.OnOffType;
25 import org.openhab.core.library.types.StringType;
26 import org.openhab.core.thing.ChannelUID;
27 import org.openhab.core.thing.binding.ThingHandler;
28 import org.openhab.core.types.Command;
29 import org.openhab.core.types.RefreshType;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 import io.netty.buffer.ByteBuf;
34 import io.netty.buffer.Unpooled;
35 import io.netty.channel.ChannelDuplexHandler;
36 import io.netty.channel.ChannelHandlerContext;
37 import io.netty.handler.codec.http.DefaultFullHttpRequest;
38 import io.netty.handler.codec.http.FullHttpRequest;
39 import io.netty.handler.codec.http.HttpHeaderNames;
40 import io.netty.handler.codec.http.HttpHeaderValues;
41 import io.netty.handler.codec.http.HttpMethod;
42 import io.netty.handler.codec.http.HttpVersion;
43 import io.netty.util.ReferenceCountUtil;
46 * The {@link HikvisionHandler} is responsible for handling commands, which are
47 * sent to one of the channels.
49 * @author Matthew Skinner - Initial contribution
53 public class HikvisionHandler extends ChannelDuplexHandler {
54 private final Logger logger = LoggerFactory.getLogger(getClass());
55 private IpCameraHandler ipCameraHandler;
56 private int nvrChannel;
57 private int lineCount, vmdCount, leftCount, takenCount, faceCount, pirCount, fieldCount;
59 public HikvisionHandler(ThingHandler handler, int nvrChannel) {
60 ipCameraHandler = (IpCameraHandler) handler;
61 this.nvrChannel = nvrChannel;
64 // This handles the incoming http replies back from the camera.
66 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
67 if (msg == null || ctx == null) {
72 String content = msg.toString();
73 logger.trace("HTTP Result back from camera is \t:{}:", content);
74 if (content.contains("--boundary")) {// Alarm checking goes in here//
75 if (content.contains("<EventNotificationAlert version=\"")) {
76 if (content.contains("hannelID>" + nvrChannel + "</")) {// some camera use c or <dynChannelID>
77 if (content.contains("<eventType>linedetection</eventType>")) {
78 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
81 if (content.contains("<eventType>fielddetection</eventType>")) {
82 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
83 fieldCount = debounce;
85 if (content.contains("<eventType>VMD</eventType>")) {
86 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
89 if (content.contains("<eventType>facedetection</eventType>")) {
90 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
93 if (content.contains("<eventType>unattendedBaggage</eventType>")) {
94 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
97 if (content.contains("<eventType>attendedBaggage</eventType>")) {
98 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
99 takenCount = debounce;
101 if (content.contains("<eventType>PIR</eventType>")) {
102 ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
105 if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
112 } else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all
114 if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
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);
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);
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);
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);
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));
163 case "AudioDetection version=":
164 ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
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);
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);
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);
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.");
200 if (content.contains("<EventNotificationAlert")) {
201 if (content.contains("hannelID>" + nvrChannel + "</")
202 || content.contains("<channelID>0</channelID>")) {// some camera use c or
204 if (content.contains(
205 "<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
215 logger.debug("Unhandled reply-{}.", content);
221 ReferenceCountUtil.release(msg);
225 // This does debouncing of the alarms
229 } else if (lineCount == 1) {
230 ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
235 } else if (vmdCount == 1) {
236 ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
241 } else if (leftCount == 1) {
242 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
245 if (takenCount > 1) {
247 } else if (takenCount == 1) {
248 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
253 } else if (faceCount == 1) {
254 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
259 } else if (pirCount == 1) {
260 ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
263 if (fieldCount > 1) {
265 } else if (fieldCount == 1) {
266 ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
269 if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
271 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
275 public void hikSendXml(String httpPutURL, String xml) {
276 logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
277 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
278 request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
279 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
280 request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
281 ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
282 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
283 request.content().clear().writeBytes(bbuf);
284 ipCameraHandler.sendHttpPUT(httpPutURL, request);
287 public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
288 ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
289 if (localTracker == null) {
290 ipCameraHandler.sendHttpGET(httpGetPutURL);
292 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
295 String body = localTracker.getReply();
296 if (body.isEmpty()) {
298 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
299 ipCameraHandler.sendHttpGET(httpGetPutURL);
301 logger.trace("An OLD reply from the camera was:{}", body);
302 if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
303 body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
305 int elementIndexStart = body.indexOf("<" + removeElement + ">");
306 int elementIndexEnd = body.indexOf("</" + removeElement + ">");
307 body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
308 + body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
309 logger.trace("Body for this PUT is going to be:{}", body);
310 localTracker.setReply(body);
311 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
313 request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
314 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
315 request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
316 ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
317 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
318 request.content().clear().writeBytes(bbuf);
319 ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
323 // This handles the commands that come from the Openhab event bus.
324 public void handleCommand(ChannelUID channelUID, Command command) {
325 if (command instanceof RefreshType) {
326 switch (channelUID.getId()) {
327 case CHANNEL_ENABLE_AUDIO_ALARM:
328 ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
330 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
331 ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
333 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
334 ipCameraHandler.logger.debug("FieldDetection command");
335 ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
337 case CHANNEL_ENABLE_MOTION_ALARM:
339 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
341 case CHANNEL_TEXT_OVERLAY:
343 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
345 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
346 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
348 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
349 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
352 return; // Return as we have handled the refresh command above and don't need to
354 } // end of "REFRESH"
355 switch (channelUID.getId()) {
356 case CHANNEL_TEXT_OVERLAY:
357 logger.debug("Changing text overlay to {}", command.toString());
358 if (command.toString().isEmpty()) {
359 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
360 "enabled", "<enabled>false</enabled>");
362 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
363 "displayText", "<displayText>" + command.toString() + "</displayText>");
364 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
365 "enabled", "<enabled>true</enabled>");
368 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
369 logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
370 if (OnOffType.ON.equals(command)) {
371 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
373 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
376 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
377 logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
378 if (OnOffType.OFF.equals(command)) {
379 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
380 "<triggering>low</triggering>");
382 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
383 "<triggering>high</triggering>");
386 case CHANNEL_ENABLE_PIR_ALARM:
387 if (OnOffType.ON.equals(command)) {
388 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
390 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
393 case CHANNEL_ENABLE_AUDIO_ALARM:
394 if (OnOffType.ON.equals(command)) {
395 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
396 "<enabled>true</enabled>");
398 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
399 "<enabled>false</enabled>");
402 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
403 if (OnOffType.ON.equals(command)) {
404 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
405 "<enabled>true</enabled>");
407 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
408 "<enabled>false</enabled>");
411 case CHANNEL_ENABLE_MOTION_ALARM:
412 if (OnOffType.ON.equals(command)) {
413 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
414 "enabled", "<enabled>true</enabled>");
416 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
417 "enabled", "<enabled>false</enabled>");
420 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
421 if (OnOffType.ON.equals(command)) {
422 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
423 "<enabled>true</enabled>");
425 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
426 "<enabled>false</enabled>");
429 case CHANNEL_ACTIVATE_ALARM_OUTPUT:
430 if (OnOffType.ON.equals(command)) {
431 hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
432 "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>high</outputState>\r\n</IOPortData>\r\n");
434 hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
435 "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>low</outputState>\r\n</IOPortData>\r\n");
441 // If a camera does not need to poll a request as often as snapshots, it can be
442 // added here. Binding steps through the list.
443 public ArrayList<String> getLowPriorityRequests() {
444 ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
445 lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + nvrChannel + "/status"); // must stay in element 0.
446 return lowPriorityRequests;