2 * Copyright (c) 2010-2023 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;
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;
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;
44 * The {@link HikvisionHandler} is responsible for handling commands, which are
45 * sent to one of the channels.
47 * @author Matthew Skinner - Initial contribution
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;
57 public HikvisionHandler(ThingHandler handler, int nvrChannel) {
58 ipCameraHandler = (IpCameraHandler) handler;
59 this.nvrChannel = nvrChannel;
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));
70 if (content.contains("<eventState>inactive</eventState>")) {
79 ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
82 case "attendedBaggage":
83 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
84 takenCount = debounce;
86 case "unattendedBaggage":
87 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
91 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
95 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
98 case "fielddetection":
99 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
100 fieldCount = debounce;
102 case "linedetection":
103 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
104 lineCount = debounce;
107 logger.debug("Unrecognised Hikvision eventType={}", eventType);
113 // This handles the incoming http replies back from the camera.
115 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
116 if (msg == null || ctx == null) {
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);
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);
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);
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);
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);
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));
167 case "AudioDetection version=":
168 ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
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);
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);
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);
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.");
206 ReferenceCountUtil.release(msg);
210 // This does debouncing of the alarms
214 } else if (lineCount == 1) {
215 ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
220 } else if (vmdCount == 1) {
221 ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
226 } else if (leftCount == 1) {
227 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
230 if (takenCount > 1) {
232 } else if (takenCount == 1) {
233 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
238 } else if (faceCount == 1) {
239 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
244 } else if (pirCount == 1) {
245 ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
248 if (fieldCount > 1) {
250 } else if (fieldCount == 1) {
251 ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
254 if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
256 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
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);
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);
277 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
280 String body = localTracker.getReply();
281 if (body.isEmpty()) {
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);
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());
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"),
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);
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");
315 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
316 ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
318 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
319 ipCameraHandler.logger.debug("FieldDetection command");
320 ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
322 case CHANNEL_ENABLE_MOTION_ALARM:
324 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
326 case CHANNEL_TEXT_OVERLAY:
328 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
330 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
331 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
333 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
334 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
337 return; // Return as we have handled the refresh command above and don't need to
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>");
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>");
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>");
358 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
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>");
367 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
368 "<triggering>high</triggering>");
371 case CHANNEL_ENABLE_PIR_ALARM:
372 if (OnOffType.ON.equals(command)) {
373 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
375 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
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>");
383 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
384 "<enabled>false</enabled>");
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>");
392 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
393 "<enabled>false</enabled>");
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>");
401 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
402 "enabled", "<enabled>false</enabled>");
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>");
410 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
411 "<enabled>false</enabled>");
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");
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");