2 * Copyright (c) 2010-2022 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
13 package org.openhab.binding.ipcamera.internal;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17 import java.nio.charset.StandardCharsets;
18 import java.util.ArrayList;
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;
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;
45 * The {@link HikvisionHandler} is responsible for handling commands, which are
46 * sent to one of the channels.
48 * @author Matthew Skinner - Initial contribution
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;
58 public HikvisionHandler(ThingHandler handler, int nvrChannel) {
59 ipCameraHandler = (IpCameraHandler) handler;
60 this.nvrChannel = nvrChannel;
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));
71 if (content.contains("<eventState>inactive</eventState>")) {
80 ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
83 case "attendedBaggage":
84 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
85 takenCount = debounce;
87 case "unattendedBaggage":
88 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
92 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
96 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
99 case "fielddetection":
100 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
101 fieldCount = debounce;
103 case "linedetection":
104 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
105 lineCount = debounce;
108 logger.debug("Unrecognised Hikvision eventType={}", eventType);
114 // This handles the incoming http replies back from the camera.
116 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
117 if (msg == null || ctx == null) {
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);
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);
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);
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);
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);
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));
168 case "AudioDetection version=":
169 ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
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);
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);
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);
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.");
207 ReferenceCountUtil.release(msg);
211 // This does debouncing of the alarms
215 } else if (lineCount == 1) {
216 ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
221 } else if (vmdCount == 1) {
222 ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
227 } else if (leftCount == 1) {
228 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
231 if (takenCount > 1) {
233 } else if (takenCount == 1) {
234 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
239 } else if (faceCount == 1) {
240 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
245 } else if (pirCount == 1) {
246 ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
249 if (fieldCount > 1) {
251 } else if (fieldCount == 1) {
252 ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
255 if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
257 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
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);
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);
278 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
281 String body = localTracker.getReply();
282 if (body.isEmpty()) {
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);
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());
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"),
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);
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");
316 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
317 ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
319 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
320 ipCameraHandler.logger.debug("FieldDetection command");
321 ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
323 case CHANNEL_ENABLE_MOTION_ALARM:
325 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
327 case CHANNEL_TEXT_OVERLAY:
329 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
331 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
332 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
334 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
335 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
338 return; // Return as we have handled the refresh command above and don't need to
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>");
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>");
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>");
359 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
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>");
368 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
369 "<triggering>high</triggering>");
372 case CHANNEL_ENABLE_PIR_ALARM:
373 if (OnOffType.ON.equals(command)) {
374 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
376 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
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>");
384 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
385 "<enabled>false</enabled>");
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>");
393 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
394 "<enabled>false</enabled>");
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>");
402 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
403 "enabled", "<enabled>false</enabled>");
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>");
411 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
412 "<enabled>false</enabled>");
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");
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");
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;