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.onvif;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17 import java.net.ConnectException;
18 import java.net.InetSocketAddress;
19 import java.nio.charset.StandardCharsets;
20 import java.security.MessageDigest;
21 import java.security.NoSuchAlgorithmException;
22 import java.security.SecureRandom;
23 import java.text.ParseException;
24 import java.text.SimpleDateFormat;
25 import java.util.ArrayList;
26 import java.util.Base64;
27 import java.util.Date;
28 import java.util.LinkedList;
29 import java.util.List;
30 import java.util.Random;
31 import java.util.TimeZone;
32 import java.util.concurrent.Executors;
33 import java.util.concurrent.ScheduledExecutorService;
34 import java.util.concurrent.TimeUnit;
35 import java.util.concurrent.atomic.AtomicInteger;
36 import java.util.concurrent.locks.ReentrantLock;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.ipcamera.internal.Helper;
41 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.types.StateOption;
45 import org.openhab.core.util.StringUtils;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import io.netty.bootstrap.Bootstrap;
50 import io.netty.buffer.ByteBuf;
51 import io.netty.buffer.Unpooled;
52 import io.netty.channel.Channel;
53 import io.netty.channel.ChannelFuture;
54 import io.netty.channel.ChannelFutureListener;
55 import io.netty.channel.ChannelInitializer;
56 import io.netty.channel.ChannelOption;
57 import io.netty.channel.ConnectTimeoutException;
58 import io.netty.channel.EventLoopGroup;
59 import io.netty.channel.nio.NioEventLoopGroup;
60 import io.netty.channel.socket.SocketChannel;
61 import io.netty.channel.socket.nio.NioSocketChannel;
62 import io.netty.handler.codec.http.DefaultFullHttpRequest;
63 import io.netty.handler.codec.http.FullHttpRequest;
64 import io.netty.handler.codec.http.HttpClientCodec;
65 import io.netty.handler.codec.http.HttpHeaderValues;
66 import io.netty.handler.codec.http.HttpMethod;
67 import io.netty.handler.codec.http.HttpVersion;
68 import io.netty.handler.timeout.IdleStateHandler;
71 * The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
73 * @author Matthew Skinner - Initial contribution
74 * @author Kai Kreuzer - Improve handling for certain cameras
78 public class OnvifConnection {
79 public enum RequestType {
89 CreatePullPointSubscription,
93 GetServiceCapabilities,
109 GetConfigurationOptions,
118 private final Logger logger = LoggerFactory.getLogger(getClass());
119 private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
120 private @Nullable Bootstrap bootstrap;
121 private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(2);
122 private ReentrantLock connecting = new ReentrantLock();
123 private String ipAddress = "";
124 private String user = "";
125 private String password = "";
126 private int onvifPort = 80;
127 public String deviceXAddr = "http://" + ipAddress + "/onvif/device_service";
128 private String eventXAddr = "http://" + ipAddress + "/onvif/device_service";
129 private String mediaXAddr = "http://" + ipAddress + "/onvif/device_service";
130 @SuppressWarnings("unused")
131 private String imagingXAddr = "http://" + ipAddress + "/onvif/device_service";
132 private String ptzXAddr = "http://" + ipAddress + "/onvif/ptz_service";
133 public String subscriptionXAddr = "http://" + ipAddress + "/onvif/device_service";
134 public String subscriptionId = "";
135 private boolean isConnected = false;
136 private int mediaProfileIndex = 0;
137 private String rtspUri = "";
138 private IpCameraHandler ipCameraHandler;
139 private boolean supportsEvents = false; // camera has replied that it can do events
140 // Use/skip events even if camera support them. API cameras skip, as their own methods give better results.
141 private boolean usingEvents = false;
142 public AtomicInteger pullMessageRequests = new AtomicInteger();
144 // These hold the cameras PTZ position in the range that the camera uses, ie
146 private Float panRangeMin = -1.0f;
147 private Float panRangeMax = 1.0f;
148 private Float tiltRangeMin = -1.0f;
149 private Float tiltRangeMax = 1.0f;
150 private Float zoomMin = 0.0f;
151 private Float zoomMax = 1.0f;
152 // These hold the PTZ values for updating openHABs controls in 0-100 range
153 private Float currentPanPercentage = 0.0f;
154 private Float currentTiltPercentage = 0.0f;
155 private Float currentZoomPercentage = 0.0f;
156 private Float currentPanCamValue = 0.0f;
157 private Float currentTiltCamValue = 0.0f;
158 private Float currentZoomCamValue = 0.0f;
159 private String ptzNodeToken = "000";
160 private String ptzConfigToken = "000";
161 private int presetTokenIndex = 0;
162 private List<String> presetTokens = new LinkedList<>();
163 private List<String> presetNames = new LinkedList<>();
164 private List<String> mediaProfileTokens = new LinkedList<>();
165 private boolean ptzDevice = true;
167 public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
168 this.ipCameraHandler = ipCameraHandler;
169 if (!ipAddress.isEmpty()) {
171 this.password = password;
172 getIPandPortFromUrl(ipAddress);
176 private String getXml(RequestType requestType) {
178 switch (requestType) {
180 return "<AbsoluteMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
181 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><Position><PanTilt x=\""
182 + currentPanCamValue + "\" y=\"" + currentTiltCamValue
183 + "\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\">\n"
184 + "</PanTilt>\n" + "<Zoom x=\"" + currentZoomCamValue
185 + "\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\">\n"
186 + "</Zoom>\n" + "</Position>\n"
187 + "<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"
188 + "</Speed></AbsoluteMove>";
189 case AddPTZConfiguration: // not tested to work yet
190 return "<AddPTZConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
191 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><ConfigurationToken>"
192 + ptzConfigToken + "</ConfigurationToken></AddPTZConfiguration>";
193 case ContinuousMoveLeft:
194 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
195 + mediaProfileTokens.get(mediaProfileIndex)
196 + "</ProfileToken><Velocity><PanTilt x=\"-0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
197 case ContinuousMoveRight:
198 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
199 + mediaProfileTokens.get(mediaProfileIndex)
200 + "</ProfileToken><Velocity><PanTilt x=\"0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
201 case ContinuousMoveUp:
202 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
203 + mediaProfileTokens.get(mediaProfileIndex)
204 + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
205 case ContinuousMoveDown:
206 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
207 + mediaProfileTokens.get(mediaProfileIndex)
208 + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
210 return "<Stop xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
211 + mediaProfileTokens.get(mediaProfileIndex)
212 + "</ProfileToken><PanTilt>true</PanTilt><Zoom>true</Zoom></Stop>";
213 case ContinuousMoveIn:
214 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
215 + mediaProfileTokens.get(mediaProfileIndex)
216 + "</ProfileToken><Velocity><Zoom x=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
217 case ContinuousMoveOut:
218 return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
219 + mediaProfileTokens.get(mediaProfileIndex)
220 + "</ProfileToken><Velocity><Zoom x=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
221 case CreatePullPointSubscription:
222 return "<CreatePullPointSubscription xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><InitialTerminationTime>PT600S</InitialTerminationTime></CreatePullPointSubscription>";
223 case GetCapabilities:
224 return "<GetCapabilities xmlns=\"http://www.onvif.org/ver10/device/wsdl\"><Category>All</Category></GetCapabilities>";
226 case GetDeviceInformation:
227 return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
229 return "<GetProfiles xmlns=\"http://www.onvif.org/ver10/media/wsdl\"/>";
230 case GetServiceCapabilities:
231 return "<GetServiceCapabilities xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></GetServiceCapabilities>";
233 return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
234 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
236 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>"
237 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStreamUri>";
238 case GetSystemDateAndTime:
239 return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
241 return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
242 + ipCameraHandler.hostIp + ":" + SERVLET_PORT + "/ipcamera/"
243 + ipCameraHandler.getThing().getUID().getId()
244 + "/OnvifEvent</Address></ConsumerReference></Subscribe>";
246 return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
248 return "<PullMessages xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><Timeout>PT8S</Timeout><MessageLimit>1</MessageLimit></PullMessages>";
249 case GetEventProperties:
250 return "<GetEventProperties xmlns=\"http://www.onvif.org/ver10/events/wsdl\"/>";
251 case RelativeMoveLeft:
252 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
253 + mediaProfileTokens.get(mediaProfileIndex)
254 + "</ProfileToken><Translation><PanTilt x=\"0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
255 case RelativeMoveRight:
256 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
257 + mediaProfileTokens.get(mediaProfileIndex)
258 + "</ProfileToken><Translation><PanTilt x=\"-0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
260 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
261 + mediaProfileTokens.get(mediaProfileIndex)
262 + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
263 case RelativeMoveDown:
264 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
265 + mediaProfileTokens.get(mediaProfileIndex)
266 + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"-0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
268 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
269 + mediaProfileTokens.get(mediaProfileIndex)
270 + "</ProfileToken><Translation><Zoom x=\"0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
271 case RelativeMoveOut:
272 return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
273 + mediaProfileTokens.get(mediaProfileIndex)
274 + "</ProfileToken><Translation><Zoom x=\"-0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
276 return "<Renew xmlns=\"http://docs.oasis-open.org/wsn/b-2\"><TerminationTime>PT10S</TerminationTime></Renew>";
277 case GetConfigurations:
278 return "<GetConfigurations xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetConfigurations>";
279 case GetConfigurationOptions:
280 return "<GetConfigurationOptions xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ConfigurationToken>"
281 + ptzConfigToken + "</ConfigurationToken></GetConfigurationOptions>";
282 case GetConfiguration:
283 return "<GetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfigurationToken>"
284 + ptzConfigToken + "</PTZConfigurationToken></GetConfiguration>";
285 case SetConfiguration:// not tested to work yet
286 return "<SetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfiguration><NodeToken>"
288 + "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
290 return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
292 return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
293 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
295 return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
296 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
297 + presetTokens.get(presetTokenIndex) + "</PresetToken></GotoPreset>";
299 return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
300 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
302 } catch (IndexOutOfBoundsException e) {
304 logger.debug("IndexOutOfBoundsException occured, camera is not connected via ONVIF: {}",
307 logger.debug("IndexOutOfBoundsException occured, {}", e.getMessage());
313 public void processReply(RequestType requestType, String message) {
314 logger.trace("ONVIF {} reply is: {}", requestType, message);
315 switch (requestType) {
316 case CreatePullPointSubscription:
317 supportsEvents = true;
318 subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>");
319 int start = message.indexOf("<dom0:SubscriptionId");
320 int end = message.indexOf("</dom0:SubscriptionId>");
321 if (start > -1 && end > start) {
322 subscriptionId = message.substring(start, end + 22);
324 logger.debug("subscriptionXAddr={} subscriptionId={}", subscriptionXAddr, subscriptionId);
325 sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
327 case GetCapabilities:
329 sendOnvifRequest(RequestType.GetProfiles, mediaXAddr);
331 case GetDeviceInformation:
334 setIsConnected(true);
335 parseProfiles(message);
336 sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
337 sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
339 sendPTZRequest(RequestType.GetNodes);
341 if (usingEvents) {// stops API cameras from getting sent ONVIF events.
342 sendOnvifRequest(RequestType.GetEventProperties, eventXAddr);
343 sendOnvifRequest(RequestType.GetServiceCapabilities, eventXAddr);
346 case GetServiceCapabilities:
347 if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
348 sendOnvifRequest(RequestType.Subscribe, eventXAddr);
352 String url = Helper.fetchXML(message, ":MediaUri", ":Uri");
353 if (!url.isBlank()) {
354 logger.debug("GetSnapshotUri: {}", url);
355 if (ipCameraHandler.snapshotUri.isEmpty()
356 && !"ffmpeg".equals(ipCameraHandler.cameraConfig.getSnapshotUrl())) {
357 ipCameraHandler.snapshotUri = ipCameraHandler.getCorrectUrlFormat(url);
358 if (ipCameraHandler.getPortFromShortenedUrl(url) != ipCameraHandler.cameraConfig.getPort()) {
360 "ONVIF is reporting the snapshot does not match the things configured port of:{}",
361 ipCameraHandler.cameraConfig.getPort());
367 String xml = StringUtils.unEscapeXml(Helper.fetchXML(message, ":MediaUri", ":Uri>"));
370 logger.debug("GetStreamUri: {}", rtspUri);
371 if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
372 ipCameraHandler.rtspUri = rtspUri;
376 case GetSystemDateAndTime:
377 setIsConnected(true);// Instar profile T only cameras need this
378 parseDateAndTime(message);
381 eventRecieved(message);
382 sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
384 case GetEventProperties:
385 sendOnvifRequest(RequestType.CreatePullPointSubscription, eventXAddr);
389 case GetConfiguration:
390 sendPTZRequest(RequestType.GetPresets);
391 ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
392 logger.debug("ptzConfigToken={}", ptzConfigToken);
393 sendPTZRequest(RequestType.GetConfigurationOptions);
396 sendPTZRequest(RequestType.GetStatus);
397 ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
398 logger.debug("ptzNodeToken={}", ptzNodeToken);
399 sendPTZRequest(RequestType.GetConfigurations);
402 processPTZLocation(message);
405 parsePresets(message);
413 * The {@link removeIPandPortFromUrl} Will throw away all text before the cameras IP, also removes the IP and the
415 * leaving just the URL.
417 * @author Matthew Skinner - Initial contribution
419 String removeIPandPortFromUrl(String url) {
420 int index = url.indexOf("//");
421 if (index != -1) {// now remove the :port
422 index = url.indexOf("/", index + 2);
425 logger.debug("We hit an issue parsing url: {}", url);
428 return url.substring(index);
431 String extractIPportFromUrl(String url) {
432 int startIndex = url.indexOf("//") + 2;
433 int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
434 if (startIndex != -1 && endIndex != -1) {
435 return url.substring(startIndex, endIndex);
437 logger.debug("We hit an issue extracting IP:PORT from url: {}", url);
441 int extractPortFromUrl(String url) {
442 int startIndex = url.indexOf("//") + 2;// skip past http://
443 startIndex = url.indexOf(":", startIndex);
444 if (startIndex == -1) {// no port defined so use port 80
447 int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
448 if (endIndex == -1) {
451 return Integer.parseInt(url.substring(startIndex + 1, endIndex));
454 void parseXAddr(String message) {
455 // Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
456 String temp = Helper.fetchXML(message, "<tt:Device", "tt:XAddr");
457 if (!temp.isEmpty()) {
459 logger.debug("deviceXAddr: {}", deviceXAddr);
461 temp = Helper.fetchXML(message, "<tt:Events", "tt:XAddr");
462 if (!temp.isEmpty()) {
463 subscriptionXAddr = eventXAddr = temp;
464 logger.debug("eventsXAddr: {}", eventXAddr);
466 temp = Helper.fetchXML(message, "<tt:Media", "tt:XAddr");
467 if (!temp.isEmpty()) {
469 logger.debug("mediaXAddr: {}", mediaXAddr);
472 ptzXAddr = Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr");
473 if (ptzXAddr.isEmpty()) {
475 logger.debug("Camera has no ONVIF PTZ support.");
476 List<org.openhab.core.thing.Channel> removeChannels = new ArrayList<>();
477 org.openhab.core.thing.Channel channel = ipCameraHandler.getThing().getChannel(CHANNEL_PAN);
478 if (channel != null) {
479 removeChannels.add(channel);
481 channel = ipCameraHandler.getThing().getChannel(CHANNEL_TILT);
482 if (channel != null) {
483 removeChannels.add(channel);
485 channel = ipCameraHandler.getThing().getChannel(CHANNEL_ZOOM);
486 if (channel != null) {
487 removeChannels.add(channel);
489 ipCameraHandler.removeChannels(removeChannels);
491 logger.debug("ptzXAddr: {}", ptzXAddr);
495 private void parseDateAndTime(String message) {
496 Date openHABTime = new Date();
497 String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
498 String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
499 String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
500 String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
501 String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
502 String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
503 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-M-d'T'H:m:s");
504 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
506 String time = year + "-" + month + "-" + day + "T" + hour + ":" + minute + ":" + second;
507 Date cameraUTC = dateFormat.parse(time);
508 long timeOffset = cameraUTC.getTime() - openHABTime.getTime();
509 logger.debug("Camera UTC dateTime is: {} openHAB time is {} time is offset by {}ms",
510 dateFormat.format(cameraUTC.getTime()), dateFormat.format(openHABTime.getTime()), timeOffset);
511 if (timeOffset > 5000 || timeOffset < -5000) {
513 "ONVIF time in camera does not match openHAB's time, this can cause authentication issues as ONVIF requires the time to be close to each other");
515 } catch (ParseException e) {
516 logger.debug("Cameras time and date could not be parsed");
520 private String getUTCdateTime() {
521 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
522 format.setTimeZone(TimeZone.getTimeZone("UTC"));
523 return format.format(new Date());
526 String createNonce() {
527 Random nonce = new SecureRandom();
528 return "" + nonce.nextInt();
531 String encodeBase64(String raw) {
532 return Base64.getEncoder().encodeToString(raw.getBytes());
535 String createDigest(String nOnce, String dateTime) {
536 String beforeEncryption = nOnce + dateTime + password;
537 MessageDigest msgDigest;
538 byte[] encryptedRaw = null;
540 msgDigest = MessageDigest.getInstance("SHA-1");
542 msgDigest.update(beforeEncryption.getBytes(StandardCharsets.UTF_8));
543 encryptedRaw = msgDigest.digest();
544 } catch (NoSuchAlgorithmException e) {
546 return Base64.getEncoder().encodeToString(encryptedRaw);
549 public void sendOnvifRequest(RequestType requestType, String xAddr) {
550 logger.trace("Sending ONVIF request: {} to {}", requestType, xAddr);
551 int port = extractPortFromUrl(xAddr);
552 String security = "";
553 String extraEnvelope = "";
554 String headerTo = "";
555 String getXmlCache = getXml(requestType);
556 if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
557 || requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
558 headerTo = "<a:To s:mustUnderstand=\"1\">" + xAddr + "</a:To>";
559 extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
562 if (!password.isEmpty() && !requestType.equals(RequestType.GetSystemDateAndTime)) {
563 String nonce = createNonce();
564 String dateTime = getUTCdateTime();
565 String digest = createDigest(nonce, dateTime);
566 security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
568 + "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
570 + "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
571 + encodeBase64(nonce)
572 + "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
573 + dateTime + "</Created></UsernameToken></Security>";
575 if (requestType.equals(RequestType.PullMessages) || requestType.equals(RequestType.Renew)) {
576 headers = "<s:Header>" + security + headerTo + subscriptionId + "</s:Header>";
578 headers = "<s:Header>" + security + headerTo + "</s:Header>";
580 } else {// GetSystemDateAndTime must not be password protected as per spec.
583 FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"),
584 removeIPandPortFromUrl(xAddr));
585 String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
586 request.headers().add("Content-Type",
587 "application/soap+xml; charset=utf-8; action=\"" + actionString + "/" + requestType + "\"");
588 request.headers().add("Charset", "utf-8");
589 // Tapo brand have different ports for the event xAddr to the other xAddr, can't use 1 port for all ONVIF calls.
590 request.headers().set("Host", ipAddress + ":" + port);
591 request.headers().set("Connection", HttpHeaderValues.CLOSE);
592 request.headers().set("Accept-Encoding", "gzip, deflate");
593 String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
595 + "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
596 + getXmlCache + "</s:Body></s:Envelope>";
597 request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
598 ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
599 request.headers().set("Content-Length", bbuf.readableBytes());
600 request.content().clear().writeBytes(bbuf);
602 Bootstrap localBootstap = bootstrap;
603 if (localBootstap == null) {
604 mainEventLoopGroup = new NioEventLoopGroup(2);
605 localBootstap = new Bootstrap();
606 localBootstap.group(mainEventLoopGroup);
607 localBootstap.channel(NioSocketChannel.class);
608 localBootstap.option(ChannelOption.SO_KEEPALIVE, true);
609 localBootstap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
610 localBootstap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
611 localBootstap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
612 localBootstap.option(ChannelOption.TCP_NODELAY, true);
613 localBootstap.handler(new ChannelInitializer<SocketChannel>() {
616 public void initChannel(SocketChannel socketChannel) throws Exception {
617 socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 18));
618 socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
619 socketChannel.pipeline().addLast(ONVIF_CODEC, new OnvifCodec(getHandle()));
622 bootstrap = localBootstap;
624 if (!mainEventLoopGroup.isShuttingDown()) {
625 // Tapo brand have different ports for the event xAddr to the other xAddr, can't use 1 port for all calls.
626 localBootstap.connect(new InetSocketAddress(ipAddress, port)).addListener(new ChannelFutureListener() {
629 public void operationComplete(@Nullable ChannelFuture future) {
630 if (future == null) {
633 if (future.isDone() && future.isSuccess()) {
634 Channel ch = future.channel();
635 OnvifCodec onvifCodec = (OnvifCodec) ch.pipeline().get(ONVIF_CODEC);
636 onvifCodec.setRequestType(requestType);
637 ch.writeAndFlush(request);
638 } else { // an error occurred
639 if (future.isDone() && !future.isCancelled()) {
640 Throwable cause = future.cause();
641 String msg = cause.getMessage();
642 logger.debug("Connect failed - cause is: {}", cause.getMessage());
643 if (cause instanceof ConnectTimeoutException) {
644 usingEvents = false;// Prevent Unsubscribe from being sent
645 ipCameraHandler.cameraCommunicationError(
646 "Camera timed out when trying to connect to the ONVIF port:" + port);
647 } else if ((cause instanceof ConnectException) && msg != null
648 && msg.contains("Connection refused")) {
649 usingEvents = false;// Prevent Unsubscribe from being sent
650 ipCameraHandler.cameraCommunicationError(
651 "Camera refused to connect when using ONVIF to port:" + port);
654 ipCameraHandler.cameraCommunicationError("Camera failed to connect due to being cancelled");
660 logger.debug("ONVIF message not sent as connection is shutting down");
664 OnvifConnection getHandle() {
668 void getIPandPortFromUrl(String url) {
669 int beginIndex = url.indexOf(":");
670 int endIndex = url.indexOf("/", beginIndex);
671 if (beginIndex >= 0 && endIndex == -1) {// 192.168.1.1:8080
672 ipAddress = url.substring(0, beginIndex);
673 onvifPort = Integer.parseInt(url.substring(beginIndex + 1));
674 } else if (beginIndex >= 0 && endIndex > beginIndex) {// 192.168.1.1:8080/foo/bar
675 ipAddress = url.substring(0, beginIndex);
676 onvifPort = Integer.parseInt(url.substring(beginIndex + 1, endIndex));
677 } else {// 192.168.1.1
679 deviceXAddr = "http://" + ipAddress + "/onvif/device_service";
680 logger.debug("No ONVIF Port found when parsing: {}", url);
683 deviceXAddr = "http://" + ipAddress + ":" + onvifPort + "/onvif/device_service";
686 public void gotoPreset(int index) {
688 if (index > 0) {// 0 is reserved for HOME as cameras seem to start at preset 1.
689 if (presetTokens.isEmpty()) {
690 logger.warn("Camera did not report any ONVIF preset locations, updating preset tokens now.");
691 sendPTZRequest(RequestType.GetPresets);
693 presetTokenIndex = index - 1;
694 sendPTZRequest(RequestType.GotoPreset);
700 public void eventRecieved(String eventMessage) {
701 String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
702 if (topic.isEmpty()) {
703 logger.trace("No ONVIF Events occured in the last 8 seconds");
706 String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
707 String dataValue = Helper.fetchXML(eventMessage, "tt:Data", "Value=\"");
708 logger.debug("ONVIF Event Topic: {}, Data: {}, Value: {}", topic, dataName, dataValue);
710 case "RuleEngine/CellMotionDetector/Motion":
711 if ("true".equals(dataValue)) {
712 ipCameraHandler.motionDetected(CHANNEL_CELL_MOTION_ALARM);
713 } else if ("false".equals(dataValue)) {
714 ipCameraHandler.noMotionDetected(CHANNEL_CELL_MOTION_ALARM);
717 case "VideoAnalytics/Motion":
718 if ("Trigger".equals(dataValue)) {
719 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
720 } else if ("Normal".equals(dataValue)) {
721 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
724 case "RuleEngine/tnsaxis:VMD3/vmd3_video_1":
725 case "RuleEngine/MotionRegionDetector/Motion":
726 case "VideoSource/MotionAlarm":
727 if ("true".equals(dataValue) || "1".equals(dataValue)) {
728 ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
729 } else if ("false".equals(dataValue) || "0".equals(dataValue)) {
730 ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
733 case "AudioAnalytics/Audio/DetectedSound":
734 if ("true".equals(dataValue)) {
735 ipCameraHandler.audioDetected();
736 } else if ("false".equals(dataValue)) {
737 ipCameraHandler.noAudioDetected();
740 case "RuleEngine/FieldDetector/ObjectsInside":
741 if ("true".equals(dataValue)) {
742 ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
743 } else if ("false".equals(dataValue)) {
744 ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
747 case "RuleEngine/LineDetector/Crossed":
748 if ("ObjectId".equals(dataName)) {
749 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
751 ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
754 case "RuleEngine/TamperDetector/Tamper":
755 if ("true".equals(dataValue)) {
756 ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.ON);
757 } else if ("false".equals(dataValue)) {
758 ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.OFF);
761 case "Device/tnsaxis:HardwareFailure/StorageFailure":
762 case "Device/HardwareFailure/StorageFailure":
763 if ("true".equals(dataValue) || "1".equals(dataValue)) {
764 ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.ON);
765 } else if ("false".equals(dataValue) || "0".equals(dataValue)) {
766 ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.OFF);
769 case "VideoSource/ImageTooDark/AnalyticsService":
770 case "VideoSource/ImageTooDark/ImagingService":
771 case "VideoSource/ImageTooDark/RecordingService":
772 if ("true".equals(dataValue)) {
773 ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON);
774 } else if ("false".equals(dataValue)) {
775 ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF);
778 case "VideoSource/GlobalSceneChange/AnalyticsService":
779 case "VideoSource/GlobalSceneChange/ImagingService":
780 case "VideoSource/GlobalSceneChange/RecordingService":
781 if ("true".equals(dataValue) || "1".equals(dataValue)) {
782 ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON);
783 } else if ("false".equals(dataValue) || "0".equals(dataValue)) {
784 ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF);
787 case "VideoSource/ImageTooBright/AnalyticsService":
788 case "VideoSource/ImageTooBright/ImagingService":
789 case "VideoSource/ImageTooBright/RecordingService":
790 if ("true".equals(dataValue)) {
791 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.ON);
792 } else if ("false".equals(dataValue)) {
793 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.OFF);
796 case "VideoSource/ImageTooBlurry/AnalyticsService":
797 case "VideoSource/ImageTooBlurry/ImagingService":
798 case "VideoSource/ImageTooBlurry/RecordingService":
799 if ("true".equals(dataValue)) {
800 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON);
801 } else if ("false".equals(dataValue)) {
802 ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF);
805 case "RuleEngine/MyRuleDetector/Visitor":
806 if ("true".equals(dataValue)) {
807 ipCameraHandler.changeAlarmState(CHANNEL_DOORBELL, OnOffType.ON);
808 } else if ("false".equals(dataValue)) {
809 ipCameraHandler.changeAlarmState(CHANNEL_DOORBELL, OnOffType.OFF);
812 case "RuleEngine/MyRuleDetector/VehicleDetect":
813 if ("true".equals(dataValue)) {
814 ipCameraHandler.changeAlarmState(CHANNEL_CAR_ALARM, OnOffType.ON);
815 } else if ("false".equals(dataValue)) {
816 ipCameraHandler.changeAlarmState(CHANNEL_CAR_ALARM, OnOffType.OFF);
819 case "RuleEngine/MyRuleDetector/DogCatDetect":
820 if ("true".equals(dataValue)) {
821 ipCameraHandler.changeAlarmState(CHANNEL_ANIMAL_ALARM, OnOffType.ON);
822 } else if ("false".equals(dataValue)) {
823 ipCameraHandler.changeAlarmState(CHANNEL_ANIMAL_ALARM, OnOffType.OFF);
826 case "RuleEngine/MyRuleDetector/FaceDetect":
827 if ("true".equals(dataValue)) {
828 ipCameraHandler.changeAlarmState(CHANNEL_FACE_DETECTED, OnOffType.ON);
829 } else if ("false".equals(dataValue)) {
830 ipCameraHandler.changeAlarmState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
833 case "RuleEngine/MyRuleDetector/PeopleDetect":
834 if ("true".equals(dataValue)) {
835 ipCameraHandler.changeAlarmState(CHANNEL_HUMAN_ALARM, OnOffType.ON);
836 } else if ("false".equals(dataValue)) {
837 ipCameraHandler.changeAlarmState(CHANNEL_HUMAN_ALARM, OnOffType.OFF);
841 logger.debug("Please report this camera has an un-implemented ONVIF event. Topic: {}", topic);
845 public boolean supportsPTZ() {
849 public void getStatus() {
851 sendPTZRequest(RequestType.GetStatus);
855 public Float getAbsolutePan() {
856 return currentPanPercentage;
859 public Float getAbsoluteTilt() {
860 return currentTiltPercentage;
863 public Float getAbsoluteZoom() {
864 return currentZoomPercentage;
867 public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
869 currentPanPercentage = panValue;
870 currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
874 public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
876 currentTiltPercentage = tiltValue;
877 currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
881 public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
883 currentZoomPercentage = zoomValue;
884 currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
888 public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
890 sendPTZRequest(RequestType.AbsoluteMove);
894 public void setSelectedMediaProfile(int mediaProfileIndex) {
895 this.mediaProfileIndex = mediaProfileIndex;
898 List<String> listOfResults(String message, String heading, String key) {
899 List<String> results = new LinkedList<>();
901 for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
902 startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
903 if (startLookingFromIndex >= 0) {
904 temp = Helper.fetchXML(message.substring(startLookingFromIndex), heading, key);
905 if (!temp.isEmpty()) {
906 logger.trace("String was found: {}", temp);
909 return results;// key string must not exist so stop looking.
911 startLookingFromIndex += temp.length();
917 void parsePresets(String message) {
918 List<StateOption> presets = new ArrayList<>();
919 int counter = 1;// Presets start at 1 not 0. HOME may be added to index 0.
920 presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
921 presetNames = listOfResults(message, "<tptz:Preset", "<tt:Name>");
922 if (presetTokens.size() != presetNames.size()) {
923 logger.warn("Camera did not report the same number of Tokens and Names for PTZ presets");
926 for (String value : presetNames) {
927 presets.add(new StateOption(Integer.toString(counter++), value));
929 ipCameraHandler.stateDescriptionProvider
930 .setStateOptions(new ChannelUID(ipCameraHandler.getThing().getUID(), CHANNEL_GOTO_PRESET), presets);
933 void parseProfiles(String message) {
934 mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
935 if (mediaProfileIndex >= mediaProfileTokens.size()) {
937 "You have set the media profile to {} when the camera reported {} profiles. Falling back to mainstream 0.",
938 mediaProfileIndex, mediaProfileTokens.size());
939 mediaProfileIndex = 0;
943 void processPTZLocation(String result) {
944 logger.debug("Processing new PTZ location now");
946 int beginIndex = result.indexOf("x=\"");
947 int endIndex = result.indexOf("\"", (beginIndex + 3));
948 if (beginIndex >= 0 && endIndex >= 0) {
949 currentPanCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
950 currentPanPercentage = (((panRangeMin - currentPanCamValue) * -1) / ((panRangeMin - panRangeMax) * -1))
952 logger.debug("Pan is updating to: {} and the cam value is {}", Math.round(currentPanPercentage),
956 "Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
960 beginIndex = result.indexOf("y=\"");
961 endIndex = result.indexOf("\"", (beginIndex + 3));
962 if (beginIndex >= 0 && endIndex >= 0) {
963 currentTiltCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
964 currentTiltPercentage = (((tiltRangeMin - currentTiltCamValue) * -1) / ((tiltRangeMin - tiltRangeMax) * -1))
966 logger.debug("Tilt is updating to: {} and the cam value is {}", Math.round(currentTiltPercentage),
967 currentTiltCamValue);
972 beginIndex = result.lastIndexOf("x=\"");
973 endIndex = result.indexOf("\"", (beginIndex + 3));
974 if (beginIndex >= 0 && endIndex >= 0) {
975 currentZoomCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
976 currentZoomPercentage = (((zoomMin - currentZoomCamValue) * -1) / ((zoomMin - zoomMax) * -1)) * 100;
977 logger.debug("Zoom is updating to: {} and the cam value is {}", Math.round(currentZoomPercentage),
978 currentZoomCamValue);
984 public void sendPTZRequest(RequestType requestType) {
986 logger.debug("ONVIF was not connected when a PTZ request was made, connecting now");
987 connect(usingEvents);
989 sendOnvifRequest(requestType, ptzXAddr);
992 public void sendEventRequest(RequestType requestType) {
993 sendOnvifRequest(requestType, eventXAddr);
996 public void connect(boolean useEvents) {
1000 logger.debug("Connecting {} to ONVIF", ipAddress);
1001 threadPool = Executors.newScheduledThreadPool(2);
1002 sendOnvifRequest(RequestType.GetSystemDateAndTime, deviceXAddr);
1003 usingEvents = useEvents;
1004 sendOnvifRequest(RequestType.GetCapabilities, deviceXAddr);
1007 connecting.unlock();
1011 public boolean isConnected() {
1016 connecting.unlock();
1020 public boolean getEventsSupported() {
1021 return supportsEvents;
1024 public void setIsConnected(boolean isConnected) {
1027 this.isConnected = isConnected;
1029 connecting.unlock();
1033 private void cleanup() {
1034 if (!isConnected && !mainEventLoopGroup.isShuttingDown()) {
1036 mainEventLoopGroup.shutdownGracefully();
1037 mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
1038 } catch (InterruptedException e) {
1039 logger.warn("ONVIF was not cleanly shutdown, due to being interrupted");
1041 logger.debug("Eventloop is shutdown: {}", mainEventLoopGroup.isShutdown());
1043 threadPool.shutdown();
1048 public void disconnect() {
1049 connecting.lock();// Lock out multiple disconnect()/connect() attempts as we try to send Unsubscribe.
1051 if (bootstrap != null) {
1052 if (isConnected && usingEvents && !mainEventLoopGroup.isShuttingDown()) {
1053 // Only makes sense to send if connected
1054 // Some cameras may continue to send events even when they can't reach a server.
1055 sendOnvifRequest(RequestType.Unsubscribe, subscriptionXAddr);
1057 // give time for the Unsubscribe request to be sent, shutdownGracefully will try to send it first.
1058 threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS);
1063 isConnected = false;// isConnected is not thread safe, connecting.lock() used as fix.
1065 connecting.unlock();