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.onvif;
16 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
18 import java.io.UnsupportedEncodingException;
19 import java.net.InetSocketAddress;
20 import java.nio.charset.StandardCharsets;
21 import java.security.MessageDigest;
22 import java.security.NoSuchAlgorithmException;
23 import java.text.SimpleDateFormat;
24 import java.util.Base64;
25 import java.util.Date;
26 import java.util.LinkedList;
27 import java.util.Random;
28 import java.util.TimeZone;
29 import java.util.concurrent.TimeUnit;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.ipcamera.internal.Helper;
34 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
35 import org.openhab.core.library.types.OnOffType;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import io.netty.bootstrap.Bootstrap;
40 import io.netty.buffer.ByteBuf;
41 import io.netty.buffer.Unpooled;
42 import io.netty.channel.Channel;
43 import io.netty.channel.ChannelFuture;
44 import io.netty.channel.ChannelFutureListener;
45 import io.netty.channel.ChannelInitializer;
46 import io.netty.channel.ChannelOption;
47 import io.netty.channel.EventLoopGroup;
48 import io.netty.channel.nio.NioEventLoopGroup;
49 import io.netty.channel.socket.SocketChannel;
50 import io.netty.channel.socket.nio.NioSocketChannel;
51 import io.netty.handler.codec.http.DefaultFullHttpRequest;
52 import io.netty.handler.codec.http.FullHttpRequest;
53 import io.netty.handler.codec.http.HttpClientCodec;
54 import io.netty.handler.codec.http.HttpHeaderValues;
55 import io.netty.handler.codec.http.HttpMethod;
56 import io.netty.handler.codec.http.HttpRequest;
57 import io.netty.handler.codec.http.HttpVersion;
58 import io.netty.handler.timeout.IdleStateHandler;
61 * The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
65 * @author Matthew Skinner - Initial contribution
69 public class OnvifConnection {
70 public static enum RequestType {
80 CreatePullPointSubscription,
84 GetServiceCapabilities,
100 GetConfigurationOptions,
109 private final Logger logger = LoggerFactory.getLogger(getClass());
110 private @Nullable Bootstrap bootstrap;
111 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
112 private String ipAddress = "";
113 private String user = "";
114 private String password = "";
115 private int onvifPort = 80;
116 private String deviceXAddr = "/onvif/device_service";
117 private String eventXAddr = "/onvif/device_service";
118 private String mediaXAddr = "/onvif/device_service";
119 @SuppressWarnings("unused")
120 private String imagingXAddr = "/onvif/device_service";
121 private String ptzXAddr = "/onvif/ptz_service";
122 private String subscriptionXAddr = "/onvif/device_service";
123 private boolean isConnected = false;
124 private int mediaProfileIndex = 0;
125 private String snapshotUri = "";
126 private String rtspUri = "";
127 private IpCameraHandler ipCameraHandler;
128 private boolean usingEvents = false;
130 // These hold the cameras PTZ position in the range that the camera uses, ie
132 private Float panRangeMin = -1.0f;
133 private Float panRangeMax = 1.0f;
134 private Float tiltRangeMin = -1.0f;
135 private Float tiltRangeMax = 1.0f;
136 private Float zoomMin = 0.0f;
137 private Float zoomMax = 1.0f;
138 // These hold the PTZ values for updating Openhabs controls in 0-100 range
139 private Float currentPanPercentage = 0.0f;
140 private Float currentTiltPercentage = 0.0f;
141 private Float currentZoomPercentage = 0.0f;
142 private Float currentPanCamValue = 0.0f;
143 private Float currentTiltCamValue = 0.0f;
144 private Float currentZoomCamValue = 0.0f;
145 private String ptzNodeToken = "000";
146 private String ptzConfigToken = "000";
147 private int presetTokenIndex = 0;
148 private LinkedList<String> presetTokens = new LinkedList<>();
149 private LinkedList<String> mediaProfileTokens = new LinkedList<>();
150 private boolean ptzDevice = true;
152 public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
153 this.ipCameraHandler = ipCameraHandler;
154 if (!ipAddress.isEmpty()) {
156 this.password = password;
157 getIPandPortFromUrl(ipAddress);
161 String getXml(RequestType requestType) {
162 switch (requestType) {
164 return "<AbsoluteMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
165 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><Position><PanTilt x=\""
166 + currentPanCamValue + "\" y=\"" + currentTiltCamValue
167 + "\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\">\n"
168 + "</PanTilt>\n" + "<Zoom x=\"" + currentZoomCamValue
169 + "\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\">\n"
170 + "</Zoom>\n" + "</Position>\n"
171 + "<Speed><PanTilt x=\"0.1\" y=\"0.1\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace\"></PanTilt><Zoom x=\"1.0\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace\"></Zoom>\n"
172 + "</Speed></AbsoluteMove>";
173 case AddPTZConfiguration: // not tested to work yet
174 return "<AddPTZConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
175 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><ConfigurationToken>"
176 + ptzConfigToken + "</ConfigurationToken></AddPTZConfiguration>";
177 case ContinuousMoveLeft:
178 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
179 + mediaProfileTokens.get(mediaProfileIndex)
180 + "</ProfileToken><Velocity><PanTilt x=\"-0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
181 case ContinuousMoveRight:
182 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
183 + mediaProfileTokens.get(mediaProfileIndex)
184 + "</ProfileToken><Velocity><PanTilt x=\"0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
185 case ContinuousMoveUp:
186 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
187 + mediaProfileTokens.get(mediaProfileIndex)
188 + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
189 case ContinuousMoveDown:
190 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
191 + mediaProfileTokens.get(mediaProfileIndex)
192 + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
194 return "<Stop xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
195 + mediaProfileTokens.get(mediaProfileIndex)
196 + "</ProfileToken><PanTilt>true</PanTilt><Zoom>true</Zoom></Stop>";
197 case ContinuousMoveIn:
198 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
199 + mediaProfileTokens.get(mediaProfileIndex)
200 + "</ProfileToken><Velocity><Zoom x=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
201 case ContinuousMoveOut:
202 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
203 + mediaProfileTokens.get(mediaProfileIndex)
204 + "</ProfileToken><Velocity><Zoom x=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
205 case CreatePullPointSubscription:
206 return "<CreatePullPointSubscription xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><InitialTerminationTime>PT600S</InitialTerminationTime></CreatePullPointSubscription>";
207 case GetCapabilities:
208 return "<GetCapabilities xmlns=\"http://www.onvif.org/ver10/device/wsdl\"><Category>All</Category></GetCapabilities>";
210 case GetDeviceInformation:
211 return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
213 return "<GetProfiles xmlns=\"http://www.onvif.org/ver10/media/wsdl\"/>";
214 case GetServiceCapabilities:
215 return "<GetServiceCapabilities xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></GetServiceCapabilities>";
217 return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
218 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
220 return "<GetStreamUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><StreamSetup><Stream xmlns=\"http://www.onvif.org/ver10/schema\">RTP-Unicast</Stream><Transport xmlns=\"http://www.onvif.org/ver10/schema\"><Protocol>RTSP</Protocol></Transport></StreamSetup><ProfileToken>"
221 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStreamUri>";
222 case GetSystemDateAndTime:
223 return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
225 return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
226 + ipCameraHandler.hostIp + ":" + ipCameraHandler.cameraConfig.getServerPort()
227 + "/OnvifEvent</Address></ConsumerReference></Subscribe>";
229 return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
231 return "<PullMessages xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><Timeout>PT8S</Timeout><MessageLimit>1</MessageLimit></PullMessages>";
232 case GetEventProperties:
233 return "<GetEventProperties xmlns=\"http://www.onvif.org/ver10/events/wsdl\"/>";
234 case RelativeMoveLeft:
235 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
236 + mediaProfileTokens.get(mediaProfileIndex)
237 + "</ProfileToken><Translation><PanTilt x=\"0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
238 case RelativeMoveRight:
239 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
240 + mediaProfileTokens.get(mediaProfileIndex)
241 + "</ProfileToken><Translation><PanTilt x=\"-0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
243 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
244 + mediaProfileTokens.get(mediaProfileIndex)
245 + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
246 case RelativeMoveDown:
247 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
248 + mediaProfileTokens.get(mediaProfileIndex)
249 + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"-0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
251 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
252 + mediaProfileTokens.get(mediaProfileIndex)
253 + "</ProfileToken><Translation><Zoom x=\"0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
254 case RelativeMoveOut:
255 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
256 + mediaProfileTokens.get(mediaProfileIndex)
257 + "</ProfileToken><Translation><Zoom x=\"-0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
259 return "<Renew xmlns=\"http://docs.oasis-open.org/wsn/b-2\"><TerminationTime>PT1M</TerminationTime></Renew>";
260 case GetConfigurations:
261 return "<GetConfigurations xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetConfigurations>";
262 case GetConfigurationOptions:
263 return "<GetConfigurationOptions xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ConfigurationToken>"
264 + ptzConfigToken + "</ConfigurationToken></GetConfigurationOptions>";
265 case GetConfiguration:
266 return "<GetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfigurationToken>"
267 + ptzConfigToken + "</PTZConfigurationToken></GetConfiguration>";
268 case SetConfiguration:// not tested to work yet
269 return "<SetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfiguration><NodeToken>"
271 + "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
273 return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
275 return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
276 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
278 return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
279 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
280 + presetTokens.get(presetTokenIndex)
281 + "</PresetToken><Speed><PanTilt x=\"0.0\" y=\"0.0\" space=\"\"></PanTilt><Zoom x=\"0.0\" space=\"\"></Zoom></Speed></GotoPreset>";
283 return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
284 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
289 public void processReply(String message) {
290 logger.trace("Onvif reply is:{}", message);
291 if (message.contains("PullMessagesResponse")) {
292 eventRecieved(message);
293 } else if (message.contains("RenewResponse")) {
294 sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr));
295 } else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
297 sendOnvifRequest(requestBuilder(RequestType.GetCapabilities, deviceXAddr));
298 parseDateAndTime(message);
299 logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime());
300 } else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
302 sendOnvifRequest(requestBuilder(RequestType.GetProfiles, mediaXAddr));
303 } else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
304 parseProfiles(message);
305 sendOnvifRequest(requestBuilder(RequestType.GetSnapshotUri, mediaXAddr));
306 sendOnvifRequest(requestBuilder(RequestType.GetStreamUri, mediaXAddr));
308 sendPTZRequest(RequestType.GetNodes);
310 if (usingEvents) {// stops API cameras from getting sent ONVIF events.
311 sendOnvifRequest(requestBuilder(RequestType.GetEventProperties, eventXAddr));
312 sendOnvifRequest(requestBuilder(RequestType.GetServiceCapabilities, eventXAddr));
314 } else if (message.contains("GetServiceCapabilitiesResponse")) {
315 if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
316 sendOnvifRequest(requestBuilder(RequestType.Subscribe, eventXAddr));
318 } else if (message.contains("GetEventPropertiesResponse")) {
319 sendOnvifRequest(requestBuilder(RequestType.CreatePullPointSubscription, eventXAddr));
320 } else if (message.contains("SubscribeResponse")) {
321 logger.info("Onvif Subscribe appears to be working for Alarms/Events.");
322 } else if (message.contains("CreatePullPointSubscriptionResponse")) {
323 subscriptionXAddr = removeIPfromUrl(Helper.fetchXML(message, "SubscriptionReference>", "Address>"));
324 logger.debug("subscriptionXAddr={}", subscriptionXAddr);
325 sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr));
326 } else if (message.contains("GetStatusResponse")) {
327 processPTZLocation(message);
328 } else if (message.contains("GetPresetsResponse")) {
329 presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
330 } else if (message.contains("GetConfigurationsResponse")) {
331 sendPTZRequest(RequestType.GetPresets);
332 ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
333 logger.debug("ptzConfigToken={}", ptzConfigToken);
334 sendPTZRequest(RequestType.GetConfigurationOptions);
335 } else if (message.contains("GetNodesResponse")) {
336 sendPTZRequest(RequestType.GetStatus);
337 ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
338 logger.debug("ptzNodeToken={}", ptzNodeToken);
339 sendPTZRequest(RequestType.GetConfigurations);
340 } else if (message.contains("GetDeviceInformationResponse")) {
341 logger.debug("GetDeviceInformationResponse recieved");
342 } else if (message.contains("GetSnapshotUriResponse")) {
343 snapshotUri = removeIPfromUrl(Helper.fetchXML(message, ":MediaUri", ":Uri"));
344 logger.debug("GetSnapshotUri:{}", snapshotUri);
345 if (ipCameraHandler.snapshotUri.isEmpty()) {
346 ipCameraHandler.snapshotUri = snapshotUri;
348 } else if (message.contains("GetStreamUriResponse")) {
349 rtspUri = Helper.fetchXML(message, ":MediaUri", ":Uri>");
350 logger.debug("GetStreamUri:{}", rtspUri);
351 if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
352 ipCameraHandler.rtspUri = rtspUri;
357 HttpRequest requestBuilder(RequestType requestType, String xAddr) {
358 logger.trace("Sending ONVIF request:{}", requestType);
359 String security = "";
360 String extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
361 String headerTo = "";
362 String getXmlCache = getXml(requestType);
363 if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
364 || requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
365 headerTo = "<a:To s:mustUnderstand=\"1\">http://" + ipAddress + xAddr + "</a:To>";
367 if (!password.isEmpty()) {
368 String nonce = createNonce();
369 String dateTime = getUTCdateTime();
370 String digest = createDigest(nonce, dateTime);
371 security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
373 + "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
375 + "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
376 + encodeBase64(nonce)
377 + "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
378 + dateTime + "</Created></UsernameToken></Security>";
380 String headers = "<s:Header>" + security + headerTo + "</s:Header>";
382 if (requestType.equals(RequestType.GetSystemDateAndTime)) {
387 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"), xAddr);
388 request.headers().add("Content-Type", "application/soap+xml");
389 request.headers().add("charset", "utf-8");
390 if (onvifPort != 80) {
391 request.headers().set("Host", ipAddress + ":" + onvifPort);
393 request.headers().set("Host", ipAddress);
395 request.headers().set("Connection", HttpHeaderValues.CLOSE);
396 request.headers().set("Accept-Encoding", "gzip, deflate");
397 String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
399 + "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
400 + getXmlCache + "</s:Body></s:Envelope>";
401 String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
402 request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
403 ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
404 request.headers().set("Content-Length", bbuf.readableBytes());
405 request.content().clear().writeBytes(bbuf);
410 * The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
414 * @author Matthew Skinner - Initial contribution
416 String removeIPfromUrl(String url) {
417 int index = url.indexOf(ipAddress);
418 if (index != -1) {// now remove the :port
419 index = url.indexOf("/", index + ipAddress.length());
422 logger.debug("We hit an issue parsing url:{}", url);
425 return url.substring(index);
428 void parseXAddr(String message) {
429 // Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
430 String temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Device", "tt:XAddr"));
431 if (!temp.isEmpty()) {
433 logger.debug("deviceXAddr:{}", deviceXAddr);
435 temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Events", "tt:XAddr"));
436 if (!temp.isEmpty()) {
437 subscriptionXAddr = eventXAddr = temp;
438 logger.debug("eventsXAddr:{}", eventXAddr);
440 temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Media", "tt:XAddr"));
441 if (!temp.isEmpty()) {
443 logger.debug("mediaXAddr:{}", mediaXAddr);
446 ptzXAddr = removeIPfromUrl(Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr"));
447 if (ptzXAddr.isEmpty()) {
449 logger.trace("Camera must not support PTZ, it failed to give a <tt:PTZ><tt:XAddr>:{}", message);
451 logger.debug("ptzXAddr:{}", ptzXAddr);
455 private void parseDateAndTime(String message) {
456 String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
457 String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
458 String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
459 logger.debug("Cameras UTC time is : {}:{}:{}", hour, minute, second);
460 String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
461 String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
462 String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
463 logger.debug("Cameras UTC date is : {}-{}-{}", year, month, day);
466 private String getUTCdateTime() {
467 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
468 format.setTimeZone(TimeZone.getTimeZone("UTC"));
469 return format.format(new Date());
472 String createNonce() {
473 Random nonce = new Random();
474 return "" + nonce.nextInt();
477 String encodeBase64(String raw) {
478 return Base64.getEncoder().encodeToString(raw.getBytes());
481 String createDigest(String nOnce, String dateTime) {
482 String beforeEncryption = nOnce + dateTime + password;
483 MessageDigest msgDigest;
484 byte[] encryptedRaw = null;
486 msgDigest = MessageDigest.getInstance("SHA-1");
488 msgDigest.update(beforeEncryption.getBytes("utf8"));
489 encryptedRaw = msgDigest.digest();
490 } catch (NoSuchAlgorithmException e) {
491 } catch (UnsupportedEncodingException e) {
493 return Base64.getEncoder().encodeToString(encryptedRaw);
496 @SuppressWarnings("null")
497 public void sendOnvifRequest(HttpRequest request) {
498 if (bootstrap == null) {
499 bootstrap = new Bootstrap();
500 bootstrap.group(mainEventLoopGroup);
501 bootstrap.channel(NioSocketChannel.class);
502 bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
503 bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
504 bootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
505 bootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
506 bootstrap.option(ChannelOption.TCP_NODELAY, true);
507 bootstrap.handler(new ChannelInitializer<SocketChannel>() {
510 public void initChannel(SocketChannel socketChannel) throws Exception {
511 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 70));
512 socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
513 socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
517 bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() {
520 public void operationComplete(@Nullable ChannelFuture future) {
521 if (future == null) {
524 if (future.isDone() && future.isSuccess()) {
525 Channel ch = future.channel();
526 ch.writeAndFlush(request);
527 } else { // an error occured
528 logger.debug("Camera is not reachable on ONVIF port:{} or the port may be wrong.", onvifPort);
537 OnvifConnection getHandle() {
541 void getIPandPortFromUrl(String url) {
542 int beginIndex = url.indexOf(":");
543 int endIndex = url.indexOf("/", beginIndex);
544 if (beginIndex >= 0 && endIndex == -1) {// 192.168.1.1:8080
545 ipAddress = url.substring(0, beginIndex);
546 onvifPort = Integer.parseInt(url.substring(beginIndex + 1));
547 } else if (beginIndex >= 0 && endIndex > beginIndex) {// 192.168.1.1:8080/foo/bar
548 ipAddress = url.substring(0, beginIndex);
549 onvifPort = Integer.parseInt(url.substring(beginIndex + 1, endIndex));
550 } else {// 192.168.1.1
552 logger.debug("No Onvif Port found when parsing:{}", url);
556 public void gotoPreset(int index) {
558 if (index > 0) {// 0 is reserved for HOME as cameras seem to start at preset 1.
559 if (presetTokens.isEmpty()) {
560 logger.warn("Camera did not report any ONVIF preset locations, updating preset tokens now.");
561 sendPTZRequest(RequestType.GetPresets);
563 presetTokenIndex = index - 1;
564 sendPTZRequest(RequestType.GotoPreset);
570 public void eventRecieved(String eventMessage) {
571 String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
572 String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
573 String dataValue = Helper.fetchXML(eventMessage, "tt:Data", "Value=\"");
574 if (!topic.isEmpty()) {
575 logger.debug("Onvif Event Topic:{}, Data:{}, Value:{}", topic, dataName, dataValue);
578 case "RuleEngine/CellMotionDetector/Motion":
579 if (dataValue.equals("true")) {
580 ipCameraHandler.motionDetected(CHANNEL_CELL_MOTION_ALARM);
581 } else if (dataValue.equals("false")) {
582 ipCameraHandler.noMotionDetected(CHANNEL_CELL_MOTION_ALARM);
585 case "VideoSource/MotionAlarm":
586 if (dataValue.equals("true")) {
587 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
588 } else if (dataValue.equals("false")) {
589 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
592 case "AudioAnalytics/Audio/DetectedSound":
593 if (dataValue.equals("true")) {
594 ipCameraHandler.audioDetected();
595 } else if (dataValue.equals("false")) {
596 ipCameraHandler.noAudioDetected();
599 case "RuleEngine/FieldDetector/ObjectsInside":
600 if (dataValue.equals("true")) {
601 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
602 } else if (dataValue.equals("false")) {
603 ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
606 case "RuleEngine/LineDetector/Crossed":
607 if (dataName.equals("ObjectId")) {
608 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
610 ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
613 case "RuleEngine/TamperDetector/Tamper":
614 if (dataValue.equals("true")) {
615 ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.ON);
616 } else if (dataValue.equals("false")) {
617 ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.OFF);
620 case "Device/HardwareFailure/StorageFailure":
621 if (dataValue.equals("true")) {
622 ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.ON);
623 } else if (dataValue.equals("false")) {
624 ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.OFF);
627 case "VideoSource/ImageTooDark/AnalyticsService":
628 case "VideoSource/ImageTooDark/ImagingService":
629 case "VideoSource/ImageTooDark/RecordingService":
630 if (dataValue.equals("true")) {
631 ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON);
632 } else if (dataValue.equals("false")) {
633 ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF);
636 case "VideoSource/GlobalSceneChange/AnalyticsService":
637 case "VideoSource/GlobalSceneChange/ImagingService":
638 case "VideoSource/GlobalSceneChange/RecordingService":
639 if (dataValue.equals("true")) {
640 ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON);
641 } else if (dataValue.equals("false")) {
642 ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF);
645 case "VideoSource/ImageTooBright/AnalyticsService":
646 case "VideoSource/ImageTooBright/ImagingService":
647 case "VideoSource/ImageTooBright/RecordingService":
648 if (dataValue.equals("true")) {
649 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.ON);
650 } else if (dataValue.equals("false")) {
651 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.OFF);
654 case "VideoSource/ImageTooBlurry/AnalyticsService":
655 case "VideoSource/ImageTooBlurry/ImagingService":
656 case "VideoSource/ImageTooBlurry/RecordingService":
657 if (dataValue.equals("true")) {
658 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON);
659 } else if (dataValue.equals("false")) {
660 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF);
665 sendOnvifRequest(requestBuilder(RequestType.Renew, subscriptionXAddr));
668 public boolean supportsPTZ() {
672 public void getStatus() {
674 sendPTZRequest(RequestType.GetStatus);
678 public Float getAbsolutePan() {
679 return currentPanPercentage;
682 public Float getAbsoluteTilt() {
683 return currentTiltPercentage;
686 public Float getAbsoluteZoom() {
687 return currentZoomPercentage;
690 public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
692 currentPanPercentage = panValue;
693 currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
697 public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
699 currentTiltPercentage = tiltValue;
700 currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
704 public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
706 currentZoomPercentage = zoomValue;
707 currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
711 public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
713 sendPTZRequest(RequestType.AbsoluteMove);
717 public void setSelectedMediaProfile(int mediaProfileIndex) {
718 this.mediaProfileIndex = mediaProfileIndex;
721 LinkedList<String> listOfResults(String message, String heading, String key) {
722 LinkedList<String> results = new LinkedList<String>();
724 for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
725 startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
726 if (startLookingFromIndex >= 0) {
727 temp = Helper.fetchXML(message.substring(startLookingFromIndex), heading, key);
728 if (!temp.isEmpty()) {
729 logger.trace("String was found:{}", temp);
731 ++startLookingFromIndex;
738 void parseProfiles(String message) {
739 mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
740 if (mediaProfileIndex >= mediaProfileTokens.size()) {
742 "You have set the media profile to {} when the camera reported {} profiles. Falling back to mainstream 0.",
743 mediaProfileIndex, mediaProfileTokens.size());
744 mediaProfileIndex = 0;
748 void processPTZLocation(String result) {
749 logger.debug("Processing new PTZ location now");
751 int beginIndex = result.indexOf("x=\"");
752 int endIndex = result.indexOf("\"", (beginIndex + 3));
753 if (beginIndex >= 0 && endIndex >= 0) {
754 currentPanCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
755 currentPanPercentage = (((panRangeMin - currentPanCamValue) * -1) / ((panRangeMin - panRangeMax) * -1))
757 logger.debug("Pan is updating to:{} and the cam value is {}", Math.round(currentPanPercentage),
761 "Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
765 beginIndex = result.indexOf("y=\"");
766 endIndex = result.indexOf("\"", (beginIndex + 3));
767 if (beginIndex >= 0 && endIndex >= 0) {
768 currentTiltCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
769 currentTiltPercentage = (((tiltRangeMin - currentTiltCamValue) * -1) / ((tiltRangeMin - tiltRangeMax) * -1))
771 logger.debug("Tilt is updating to:{} and the cam value is {}", Math.round(currentTiltPercentage),
772 currentTiltCamValue);
777 beginIndex = result.lastIndexOf("x=\"");
778 endIndex = result.indexOf("\"", (beginIndex + 3));
779 if (beginIndex >= 0 && endIndex >= 0) {
780 currentZoomCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
781 currentZoomPercentage = (((zoomMin - currentZoomCamValue) * -1) / ((zoomMin - zoomMax) * -1)) * 100;
782 logger.debug("Zoom is updating to:{} and the cam value is {}", Math.round(currentZoomPercentage),
783 currentZoomCamValue);
789 public void sendPTZRequest(RequestType requestType) {
790 sendOnvifRequest(requestBuilder(requestType, ptzXAddr));
793 public void sendEventRequest(RequestType requestType) {
794 sendOnvifRequest(requestBuilder(requestType, eventXAddr));
797 public void connect(boolean useEvents) {
799 sendOnvifRequest(requestBuilder(RequestType.GetSystemDateAndTime, deviceXAddr));
800 usingEvents = useEvents;
804 public boolean isConnected() {
808 public void disconnect() {
809 if (usingEvents && isConnected) {
810 sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr));
813 } catch (InterruptedException e) {
817 presetTokens.clear();
818 mediaProfileTokens.clear();
819 if (!mainEventLoopGroup.isShutdown()) {
821 mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
822 } catch (InterruptedException e) {
823 logger.info("Onvif was not shutdown correctly due to being interrupted");
825 mainEventLoopGroup = new NioEventLoopGroup();