2 * Copyright (c) 2010-2024 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;
19 import java.util.List;
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;
58 private String requestUrl = "";
60 public HikvisionHandler(ThingHandler handler, int nvrChannel) {
61 ipCameraHandler = (IpCameraHandler) handler;
62 this.nvrChannel = nvrChannel;
65 private void processEvent(String content) {
66 // some cameras use <dynChannelID> or <channelID> and NVRs use channel 0 to say all channels
67 if (content.contains("hannelID>" + nvrChannel) || content.contains("<channelID>0</channelID>")) {
68 final int debounce = 3;
69 String eventType = Helper.fetchXML(content, "", "<eventType>");
70 ipCameraHandler.setChannelState(CHANNEL_LAST_EVENT_DATA, new StringType(content));
73 if (content.contains("<eventState>inactive</eventState>")) {
82 ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
85 case "attendedBaggage":
86 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
87 takenCount = debounce;
89 case "unattendedBaggage":
90 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
94 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
98 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
101 case "fielddetection":
102 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
103 fieldCount = debounce;
105 case "linedetection":
106 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
107 lineCount = debounce;
110 logger.debug("Unrecognised Hikvision eventType={}", eventType);
116 public void setURL(String url) {
120 // This handles the incoming http replies back from the camera.
122 public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
123 if (msg == null || ctx == null) {
127 String content = msg.toString();
128 logger.trace("HTTP Result from {} contains \t:{}:", requestUrl, content);
129 switch (requestUrl) {
130 case "/ISAPI/Event/notification/alertStream":
131 int startIndex = content.indexOf("<");// skip to start of XML content
132 if (startIndex != -1) {
133 String eventData = content.substring(startIndex, content.length());
134 processEvent(eventData);
137 case "/ISAPI/System/IO/capabilities": // Used to check if the camera supports IO
138 List<org.openhab.core.thing.Channel> removeChannels = new ArrayList<>();
139 org.openhab.core.thing.Channel channel;
140 if (content.contains("<IOOutputPortNums>0<") || !content.contains("<IOOutputPortNums>")) {
141 logger.debug("Camera does not support IO outputs.");
142 channel = ipCameraHandler.getThing().getChannel(CHANNEL_ACTIVATE_ALARM_OUTPUT);
143 if (channel != null) {
144 removeChannels.add(channel);
146 channel = ipCameraHandler.getThing().getChannel(CHANNEL_ACTIVATE_ALARM_OUTPUT2);
147 if (channel != null) {
148 removeChannels.add(channel);
150 } else if (content.contains("<IOOutputPortNums>1<")) {
151 channel = ipCameraHandler.getThing().getChannel(CHANNEL_ACTIVATE_ALARM_OUTPUT2);
152 if (channel != null) {
153 removeChannels.add(channel);
156 ipCameraHandler.lowPriorityRequests.clear(); // no longer need to check if the IO is supported.
157 if (content.contains("<IOInputPortNums>0<") || !content.contains("<IOInputPortNums>")) {
158 logger.debug("Camera does not support IO inputs.");
159 channel = ipCameraHandler.getThing().getChannel(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT);
160 if (channel != null) {
161 removeChannels.add(channel);
163 channel = ipCameraHandler.getThing().getChannel(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT);
164 if (channel != null) {
165 removeChannels.add(channel);
167 channel = ipCameraHandler.getThing().getChannel(CHANNEL_EXTERNAL_ALARM_INPUT);
168 if (channel != null) {
169 removeChannels.add(channel);
171 channel = ipCameraHandler.getThing().getChannel(CHANNEL_EXTERNAL_ALARM_INPUT2);
172 if (channel != null) {
173 removeChannels.add(channel);
175 } else if (content.contains("<IOInputPortNums>1<")) {
176 channel = ipCameraHandler.getThing().getChannel(CHANNEL_EXTERNAL_ALARM_INPUT2);
177 if (channel != null) {
178 removeChannels.add(channel);
180 // start checking the input IO status
181 ipCameraHandler.lowPriorityRequests.set(0,
182 "/ISAPI/System/IO/inputs/" + ipCameraHandler.cameraConfig.getNvrChannel() + "/status");
184 // start checking the input IO status
185 ipCameraHandler.lowPriorityRequests.set(0,
186 "/ISAPI/System/IO/inputs/" + ipCameraHandler.cameraConfig.getNvrChannel() + "/status");
188 ipCameraHandler.removeChannels(removeChannels);
191 String replyElement = Helper.fetchXML(content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<");
192 switch (replyElement) {
193 case "MotionDetection version=":
194 ipCameraHandler.storeHttpReply(
195 "/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection", content);
196 if (content.contains("<enabled>true</enabled>")) {
197 ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
198 } else if (content.contains("<enabled>false</enabled>")) {
199 ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
202 case "IOInputPort version=":
203 ipCameraHandler.storeHttpReply("/ISAPI/System/IO/inputs/" + nvrChannel, content);
204 if (content.contains("<enabled>true</enabled>")) {
205 ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
206 } else if (content.contains("<enabled>false</enabled>")) {
207 ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
209 if (content.contains("<triggering>low</triggering>")) {
210 ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
211 } else if (content.contains("<triggering>high</triggering>")) {
212 ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.ON);
215 case "LineDetection":
216 ipCameraHandler.storeHttpReply("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", content);
217 if (content.contains("<enabled>true</enabled>")) {
218 ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
219 } else if (content.contains("<enabled>false</enabled>")) {
220 ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
223 case "TextOverlay version=":
224 ipCameraHandler.storeHttpReply(
225 "/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1", content);
226 String text = Helper.fetchXML(content, "<enabled>true</enabled>", "<displayText>");
227 ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(text));
229 case "AudioDetection version=":
230 ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
232 if (content.contains("<enabled>true</enabled>")) {
233 ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
234 } else if (content.contains("<enabled>false</enabled>")) {
235 ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
238 case "IOPortStatus version=":
239 if (content.contains("<ioState>active</ioState>")) {
240 ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
241 } else if (content.contains("<ioState>inactive</ioState>")) {
242 ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
245 case "FieldDetection version=":
246 ipCameraHandler.storeHttpReply("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", content);
247 if (content.contains("<enabled>true</enabled>")) {
248 ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.ON);
249 } else if (content.contains("<enabled>false</enabled>")) {
250 ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.OFF);
253 case "ResponseStatus version=":
254 ////////////////// External Alarm Input ///////////////
256 .contains("<requestURL>/ISAPI/System/IO/inputs/" + nvrChannel + "/status</requestURL>")) {
257 // Stops checking the external alarm if camera does not have feature.
258 if (content.contains("<statusString>Invalid Operation</statusString>")) {
259 ipCameraHandler.lowPriorityRequests.remove(0);
260 ipCameraHandler.logger.debug(
261 "Stopping checks for alarm inputs as camera appears to be missing this feature.");
268 ReferenceCountUtil.release(msg);
272 // This does debouncing of the alarms
276 } else if (lineCount == 1) {
277 ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
282 } else if (vmdCount == 1) {
283 ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
288 } else if (leftCount == 1) {
289 ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
292 if (takenCount > 1) {
294 } else if (takenCount == 1) {
295 ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
300 } else if (faceCount == 1) {
301 ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
306 } else if (pirCount == 1) {
307 ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
310 if (fieldCount > 1) {
312 } else if (fieldCount == 1) {
313 ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
316 if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
318 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
322 public void hikSendXml(String httpPutURL, String xml) {
323 logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
324 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
325 request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
326 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
327 request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
328 ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
329 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
330 request.content().clear().writeBytes(bbuf);
331 ipCameraHandler.sendHttpPUT(httpPutURL, request);
334 public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
335 ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
336 if (localTracker == null) {
337 ipCameraHandler.sendHttpGET(httpGetPutURL);
339 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
342 String body = localTracker.getReply();
343 if (body.isEmpty()) {
345 "Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
346 ipCameraHandler.sendHttpGET(httpGetPutURL);
348 logger.trace("An OLD reply from the camera was:{}", body);
349 if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
350 body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
352 int elementIndexStart = body.indexOf("<" + removeElement + ">");
353 int elementIndexEnd = body.indexOf("</" + removeElement + ">");
354 body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
355 + body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
356 logger.trace("Body for this PUT is going to be:{}", body);
357 localTracker.setReply(body);
358 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
360 request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
361 request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
362 request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
363 ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
364 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
365 request.content().clear().writeBytes(bbuf);
366 ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
370 // This handles the commands that come from the Openhab event bus.
371 public void handleCommand(ChannelUID channelUID, Command command) {
372 if (command instanceof RefreshType) {
373 switch (channelUID.getId()) {
374 case CHANNEL_ENABLE_AUDIO_ALARM:
375 ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
377 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
378 ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
380 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
381 ipCameraHandler.logger.debug("FieldDetection command");
382 ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
384 case CHANNEL_ENABLE_MOTION_ALARM:
386 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
388 case CHANNEL_TEXT_OVERLAY:
390 .sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
392 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
393 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
395 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
396 ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
399 return; // Return as we have handled the refresh command above and don't need to
401 } // end of "REFRESH"
402 switch (channelUID.getId()) {
403 case CHANNEL_TEXT_OVERLAY:
404 logger.debug("Changing text overlay to {}", command.toString());
405 if (command.toString().isEmpty()) {
406 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
407 "enabled", "<enabled>false</enabled>");
409 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
410 "displayText", "<displayText>" + command.toString() + "</displayText>");
411 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
412 "enabled", "<enabled>true</enabled>");
415 case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
416 logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
417 if (OnOffType.ON.equals(command)) {
418 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
420 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
423 case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
424 logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
425 if (OnOffType.OFF.equals(command)) {
426 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
427 "<triggering>low</triggering>");
429 hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
430 "<triggering>high</triggering>");
433 case CHANNEL_ENABLE_PIR_ALARM:
434 if (OnOffType.ON.equals(command)) {
435 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
437 hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
440 case CHANNEL_ENABLE_AUDIO_ALARM:
441 if (OnOffType.ON.equals(command)) {
442 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
443 "<enabled>true</enabled>");
445 hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
446 "<enabled>false</enabled>");
449 case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
450 if (OnOffType.ON.equals(command)) {
451 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
452 "<enabled>true</enabled>");
454 hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
455 "<enabled>false</enabled>");
458 case CHANNEL_ENABLE_MOTION_ALARM:
459 if (OnOffType.ON.equals(command)) {
460 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
461 "enabled", "<enabled>true</enabled>");
463 hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
464 "enabled", "<enabled>false</enabled>");
467 case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
468 if (OnOffType.ON.equals(command)) {
469 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
470 "<enabled>true</enabled>");
472 hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
473 "<enabled>false</enabled>");
476 case CHANNEL_ACTIVATE_ALARM_OUTPUT:
477 if (OnOffType.ON.equals(command)) {
478 hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
479 "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>high</outputState>\r\n</IOPortData>\r\n");
481 hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
482 "<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>low</outputState>\r\n</IOPortData>\r\n");