]> git.basschouten.com Git - openhab-addons.git/blob
70b830ab394b2f70b3e301f0753050d0d0ce94d7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.ipcamera.internal.onvif;
14
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
16
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.SimpleDateFormat;
24 import java.util.ArrayList;
25 import java.util.Base64;
26 import java.util.Date;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Random;
30 import java.util.TimeZone;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.locks.ReentrantLock;
35
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.ipcamera.internal.Helper;
39 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.types.StateOption;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import io.netty.bootstrap.Bootstrap;
47 import io.netty.buffer.ByteBuf;
48 import io.netty.buffer.Unpooled;
49 import io.netty.channel.Channel;
50 import io.netty.channel.ChannelFuture;
51 import io.netty.channel.ChannelFutureListener;
52 import io.netty.channel.ChannelInitializer;
53 import io.netty.channel.ChannelOption;
54 import io.netty.channel.ConnectTimeoutException;
55 import io.netty.channel.EventLoopGroup;
56 import io.netty.channel.nio.NioEventLoopGroup;
57 import io.netty.channel.socket.SocketChannel;
58 import io.netty.channel.socket.nio.NioSocketChannel;
59 import io.netty.handler.codec.http.DefaultFullHttpRequest;
60 import io.netty.handler.codec.http.FullHttpRequest;
61 import io.netty.handler.codec.http.HttpClientCodec;
62 import io.netty.handler.codec.http.HttpHeaderValues;
63 import io.netty.handler.codec.http.HttpMethod;
64 import io.netty.handler.codec.http.HttpVersion;
65 import io.netty.handler.timeout.IdleStateHandler;
66
67 /**
68  * The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
69  *
70  *
71  *
72  * @author Matthew Skinner - Initial contribution
73  */
74
75 @NonNullByDefault
76 public class OnvifConnection {
77     public enum RequestType {
78         AbsoluteMove,
79         AddPTZConfiguration,
80         ContinuousMoveLeft,
81         ContinuousMoveRight,
82         ContinuousMoveUp,
83         ContinuousMoveDown,
84         Stop,
85         ContinuousMoveIn,
86         ContinuousMoveOut,
87         CreatePullPointSubscription,
88         GetCapabilities,
89         GetDeviceInformation,
90         GetProfiles,
91         GetServiceCapabilities,
92         GetSnapshotUri,
93         GetStreamUri,
94         GetSystemDateAndTime,
95         Subscribe,
96         Unsubscribe,
97         PullMessages,
98         GetEventProperties,
99         RelativeMoveLeft,
100         RelativeMoveRight,
101         RelativeMoveUp,
102         RelativeMoveDown,
103         RelativeMoveIn,
104         RelativeMoveOut,
105         Renew,
106         GetConfigurations,
107         GetConfigurationOptions,
108         GetConfiguration,
109         SetConfiguration,
110         GetNodes,
111         GetStatus,
112         GotoPreset,
113         GetPresets
114     }
115
116     private final Logger logger = LoggerFactory.getLogger(getClass());
117     private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
118     private @Nullable Bootstrap bootstrap;
119     private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(2);
120     private ReentrantLock connecting = new ReentrantLock();
121     private String ipAddress = "";
122     private String user = "";
123     private String password = "";
124     private int onvifPort = 80;
125     private String deviceXAddr = "http://" + ipAddress + "/onvif/device_service";
126     private String eventXAddr = "http://" + ipAddress + "/onvif/device_service";
127     private String mediaXAddr = "http://" + ipAddress + "/onvif/device_service";
128     @SuppressWarnings("unused")
129     private String imagingXAddr = "http://" + ipAddress + "/onvif/device_service";
130     private String ptzXAddr = "http://" + ipAddress + "/onvif/ptz_service";
131     private String subscriptionXAddr = "http://" + ipAddress + "/onvif/device_service";
132     private boolean connectError = false;
133     private boolean refusedError = false;
134     private boolean isConnected = false;
135     private int mediaProfileIndex = 0;
136     private String snapshotUri = "";
137     private String rtspUri = "";
138     private IpCameraHandler ipCameraHandler;
139     private boolean usingEvents = false;
140
141     // These hold the cameras PTZ position in the range that the camera uses, ie
142     // mine is -1 to +1
143     private Float panRangeMin = -1.0f;
144     private Float panRangeMax = 1.0f;
145     private Float tiltRangeMin = -1.0f;
146     private Float tiltRangeMax = 1.0f;
147     private Float zoomMin = 0.0f;
148     private Float zoomMax = 1.0f;
149     // These hold the PTZ values for updating Openhabs controls in 0-100 range
150     private Float currentPanPercentage = 0.0f;
151     private Float currentTiltPercentage = 0.0f;
152     private Float currentZoomPercentage = 0.0f;
153     private Float currentPanCamValue = 0.0f;
154     private Float currentTiltCamValue = 0.0f;
155     private Float currentZoomCamValue = 0.0f;
156     private String ptzNodeToken = "000";
157     private String ptzConfigToken = "000";
158     private int presetTokenIndex = 0;
159     private List<String> presetTokens = new LinkedList<>();
160     private List<String> presetNames = new LinkedList<>();
161     private List<String> mediaProfileTokens = new LinkedList<>();
162     private boolean ptzDevice = true;
163
164     public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
165         this.ipCameraHandler = ipCameraHandler;
166         if (!ipAddress.isEmpty()) {
167             this.user = user;
168             this.password = password;
169             getIPandPortFromUrl(ipAddress);
170         }
171     }
172
173     private String getXml(RequestType requestType) {
174         try {
175             switch (requestType) {
176                 case AbsoluteMove:
177                     return "<AbsoluteMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
178                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><Position><PanTilt x=\""
179                             + currentPanCamValue + "\" y=\"" + currentTiltCamValue
180                             + "\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\">\n"
181                             + "</PanTilt>\n" + "<Zoom x=\"" + currentZoomCamValue
182                             + "\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\">\n"
183                             + "</Zoom>\n" + "</Position>\n"
184                             + "<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"
185                             + "</Speed></AbsoluteMove>";
186                 case AddPTZConfiguration: // not tested to work yet
187                     return "<AddPTZConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
188                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><ConfigurationToken>"
189                             + ptzConfigToken + "</ConfigurationToken></AddPTZConfiguration>";
190                 case ContinuousMoveLeft:
191                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
192                             + mediaProfileTokens.get(mediaProfileIndex)
193                             + "</ProfileToken><Velocity><PanTilt x=\"-0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
194                 case ContinuousMoveRight:
195                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
196                             + mediaProfileTokens.get(mediaProfileIndex)
197                             + "</ProfileToken><Velocity><PanTilt x=\"0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
198                 case ContinuousMoveUp:
199                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
200                             + mediaProfileTokens.get(mediaProfileIndex)
201                             + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
202                 case ContinuousMoveDown:
203                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
204                             + mediaProfileTokens.get(mediaProfileIndex)
205                             + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
206                 case Stop:
207                     return "<Stop xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
208                             + mediaProfileTokens.get(mediaProfileIndex)
209                             + "</ProfileToken><PanTilt>true</PanTilt><Zoom>true</Zoom></Stop>";
210                 case ContinuousMoveIn:
211                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
212                             + mediaProfileTokens.get(mediaProfileIndex)
213                             + "</ProfileToken><Velocity><Zoom x=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
214                 case ContinuousMoveOut:
215                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
216                             + mediaProfileTokens.get(mediaProfileIndex)
217                             + "</ProfileToken><Velocity><Zoom x=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
218                 case CreatePullPointSubscription:
219                     return "<CreatePullPointSubscription xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><InitialTerminationTime>PT600S</InitialTerminationTime></CreatePullPointSubscription>";
220                 case GetCapabilities:
221                     return "<GetCapabilities xmlns=\"http://www.onvif.org/ver10/device/wsdl\"><Category>All</Category></GetCapabilities>";
222
223                 case GetDeviceInformation:
224                     return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
225                 case GetProfiles:
226                     return "<GetProfiles xmlns=\"http://www.onvif.org/ver10/media/wsdl\"/>";
227                 case GetServiceCapabilities:
228                     return "<GetServiceCapabilities xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></GetServiceCapabilities>";
229                 case GetSnapshotUri:
230                     return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
231                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
232                 case GetStreamUri:
233                     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>"
234                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStreamUri>";
235                 case GetSystemDateAndTime:
236                     return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
237                 case Subscribe:
238                     return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
239                             + ipCameraHandler.hostIp + ":" + SERVLET_PORT + "/ipcamera/"
240                             + ipCameraHandler.getThing().getUID().getId()
241                             + "/OnvifEvent</Address></ConsumerReference></Subscribe>";
242                 case Unsubscribe:
243                     return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
244                 case PullMessages:
245                     return "<PullMessages xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><Timeout>PT8S</Timeout><MessageLimit>1</MessageLimit></PullMessages>";
246                 case GetEventProperties:
247                     return "<GetEventProperties xmlns=\"http://www.onvif.org/ver10/events/wsdl\"/>";
248                 case RelativeMoveLeft:
249                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
250                             + mediaProfileTokens.get(mediaProfileIndex)
251                             + "</ProfileToken><Translation><PanTilt x=\"0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
252                 case RelativeMoveRight:
253                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
254                             + mediaProfileTokens.get(mediaProfileIndex)
255                             + "</ProfileToken><Translation><PanTilt x=\"-0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
256                 case RelativeMoveUp:
257                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
258                             + mediaProfileTokens.get(mediaProfileIndex)
259                             + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
260                 case RelativeMoveDown:
261                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
262                             + mediaProfileTokens.get(mediaProfileIndex)
263                             + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"-0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
264                 case RelativeMoveIn:
265                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
266                             + mediaProfileTokens.get(mediaProfileIndex)
267                             + "</ProfileToken><Translation><Zoom x=\"0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
268                 case RelativeMoveOut:
269                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
270                             + mediaProfileTokens.get(mediaProfileIndex)
271                             + "</ProfileToken><Translation><Zoom x=\"-0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
272                 case Renew:
273                     return "<Renew xmlns=\"http://docs.oasis-open.org/wsn/b-2\"><TerminationTime>PT1M</TerminationTime></Renew>";
274                 case GetConfigurations:
275                     return "<GetConfigurations xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetConfigurations>";
276                 case GetConfigurationOptions:
277                     return "<GetConfigurationOptions xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ConfigurationToken>"
278                             + ptzConfigToken + "</ConfigurationToken></GetConfigurationOptions>";
279                 case GetConfiguration:
280                     return "<GetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfigurationToken>"
281                             + ptzConfigToken + "</PTZConfigurationToken></GetConfiguration>";
282                 case SetConfiguration:// not tested to work yet
283                     return "<SetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfiguration><NodeToken>"
284                             + ptzNodeToken
285                             + "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
286                 case GetNodes:
287                     return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
288                 case GetStatus:
289                     return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
290                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
291                 case GotoPreset:
292                     return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
293                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
294                             + presetTokens.get(presetTokenIndex) + "</PresetToken></GotoPreset>";
295                 case GetPresets:
296                     return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
297                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
298             }
299         } catch (IndexOutOfBoundsException e) {
300             if (!isConnected) {
301                 logger.debug("IndexOutOfBoundsException occured, camera is not connected via ONVIF: {}",
302                         e.getMessage());
303             } else {
304                 logger.debug("IndexOutOfBoundsException occured, {}", e.getMessage());
305             }
306         }
307         return "notfound";
308     }
309
310     public void processReply(String message) {
311         logger.trace("Onvif reply is:{}", message);
312         if (message.contains("PullMessagesResponse")) {
313             eventRecieved(message);
314         } else if (message.contains("RenewResponse")) {
315             sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
316         } else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
317             setIsConnected(true);
318             sendOnvifRequest(RequestType.GetCapabilities, deviceXAddr);
319             parseDateAndTime(message);
320             logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime());
321         } else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
322             parseXAddr(message);
323             sendOnvifRequest(RequestType.GetProfiles, mediaXAddr);
324         } else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
325             setIsConnected(true);
326             parseProfiles(message);
327             sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
328             sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
329             if (ptzDevice) {
330                 sendPTZRequest(RequestType.GetNodes);
331             }
332             if (usingEvents) {// stops API cameras from getting sent ONVIF events.
333                 sendOnvifRequest(RequestType.GetEventProperties, eventXAddr);
334                 sendOnvifRequest(RequestType.GetServiceCapabilities, eventXAddr);
335             }
336         } else if (message.contains("GetServiceCapabilitiesResponse")) {
337             if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
338                 sendOnvifRequest(RequestType.Subscribe, eventXAddr);
339             }
340         } else if (message.contains("GetEventPropertiesResponse")) {
341             sendOnvifRequest(RequestType.CreatePullPointSubscription, eventXAddr);
342         } else if (message.contains("CreatePullPointSubscriptionResponse")) {
343             subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>");
344             logger.debug("subscriptionXAddr={}", subscriptionXAddr);
345             sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
346         } else if (message.contains("GetStatusResponse")) {
347             processPTZLocation(message);
348         } else if (message.contains("GetPresetsResponse")) {
349             parsePresets(message);
350         } else if (message.contains("GetConfigurationsResponse")) {
351             sendPTZRequest(RequestType.GetPresets);
352             ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
353             logger.debug("ptzConfigToken={}", ptzConfigToken);
354             sendPTZRequest(RequestType.GetConfigurationOptions);
355         } else if (message.contains("GetNodesResponse")) {
356             sendPTZRequest(RequestType.GetStatus);
357             ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
358             logger.debug("ptzNodeToken={}", ptzNodeToken);
359             sendPTZRequest(RequestType.GetConfigurations);
360         } else if (message.contains("GetDeviceInformationResponse")) {
361             logger.debug("GetDeviceInformationResponse recieved");
362         } else if (message.contains("GetSnapshotUriResponse")) {
363             snapshotUri = removeIPfromUrl(Helper.fetchXML(message, ":MediaUri", ":Uri"));
364             logger.debug("GetSnapshotUri:{}", snapshotUri);
365             if (ipCameraHandler.snapshotUri.isEmpty()
366                     && !"ffmpeg".equals(ipCameraHandler.cameraConfig.getSnapshotUrl())) {
367                 ipCameraHandler.snapshotUri = snapshotUri;
368             }
369         } else if (message.contains("GetStreamUriResponse")) {
370             rtspUri = Helper.fetchXML(message, ":MediaUri", ":Uri>");
371             logger.debug("GetStreamUri:{}", rtspUri);
372             if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
373                 ipCameraHandler.rtspUri = rtspUri;
374             }
375         }
376     }
377
378     /**
379      * The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
380      * leaving just the URL.
381      *
382      * @author Matthew Skinner - Initial contribution
383      */
384     String removeIPfromUrl(String url) {
385         int index = url.indexOf("//");
386         if (index != -1) {// now remove the :port
387             index = url.indexOf("/", index + 2);
388         }
389         if (index == -1) {
390             logger.debug("We hit an issue parsing url:{}", url);
391             return "";
392         }
393         return url.substring(index);
394     }
395
396     String extractIPportFromUrl(String url) {
397         int startIndex = url.indexOf("//") + 2;
398         int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
399         if (startIndex != -1 && endIndex != -1) {
400             return url.substring(startIndex, endIndex);
401         }
402         logger.debug("We hit an issue extracting IP:PORT from url:{}", url);
403         return "";
404     }
405
406     int extractPortFromUrl(String url) {
407         int startIndex = url.indexOf("//") + 2;// skip past http://
408         startIndex = url.indexOf(":", startIndex);
409         if (startIndex == -1) {// no port defined so use port 80
410             return 80;
411         }
412         int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
413         if (endIndex == -1) {
414             return 80;
415         }
416         return Integer.parseInt(url.substring(startIndex + 1, endIndex));
417     }
418
419     void parseXAddr(String message) {
420         // Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
421         String temp = Helper.fetchXML(message, "<tt:Device", "tt:XAddr");
422         if (!temp.isEmpty()) {
423             deviceXAddr = temp;
424             logger.debug("deviceXAddr:{}", deviceXAddr);
425         }
426         temp = Helper.fetchXML(message, "<tt:Events", "tt:XAddr");
427         if (!temp.isEmpty()) {
428             subscriptionXAddr = eventXAddr = temp;
429             logger.debug("eventsXAddr:{}", eventXAddr);
430         }
431         temp = Helper.fetchXML(message, "<tt:Media", "tt:XAddr");
432         if (!temp.isEmpty()) {
433             mediaXAddr = temp;
434             logger.debug("mediaXAddr:{}", mediaXAddr);
435         }
436
437         ptzXAddr = Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr");
438         if (ptzXAddr.isEmpty()) {
439             ptzDevice = false;
440             logger.debug("Camera has no ONVIF PTZ support.");
441             List<org.openhab.core.thing.Channel> removeChannels = new ArrayList<>();
442             org.openhab.core.thing.Channel channel = ipCameraHandler.getThing().getChannel(CHANNEL_PAN);
443             if (channel != null) {
444                 removeChannels.add(channel);
445             }
446             channel = ipCameraHandler.getThing().getChannel(CHANNEL_TILT);
447             if (channel != null) {
448                 removeChannels.add(channel);
449             }
450             channel = ipCameraHandler.getThing().getChannel(CHANNEL_ZOOM);
451             if (channel != null) {
452                 removeChannels.add(channel);
453             }
454             ipCameraHandler.removeChannels(removeChannels);
455         } else {
456             logger.debug("ptzXAddr:{}", ptzXAddr);
457         }
458     }
459
460     private void parseDateAndTime(String message) {
461         String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
462         String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
463         String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
464         String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
465         String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
466         String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
467         logger.debug("Cameras  UTC dateTime is:{}-{}-{}T{}:{}:{}", year, month, day, hour, minute, second);
468     }
469
470     private String getUTCdateTime() {
471         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
472         format.setTimeZone(TimeZone.getTimeZone("UTC"));
473         return format.format(new Date());
474     }
475
476     String createNonce() {
477         Random nonce = new SecureRandom();
478         return "" + nonce.nextInt();
479     }
480
481     String encodeBase64(String raw) {
482         return Base64.getEncoder().encodeToString(raw.getBytes());
483     }
484
485     String createDigest(String nOnce, String dateTime) {
486         String beforeEncryption = nOnce + dateTime + password;
487         MessageDigest msgDigest;
488         byte[] encryptedRaw = null;
489         try {
490             msgDigest = MessageDigest.getInstance("SHA-1");
491             msgDigest.reset();
492             msgDigest.update(beforeEncryption.getBytes(StandardCharsets.UTF_8));
493             encryptedRaw = msgDigest.digest();
494         } catch (NoSuchAlgorithmException e) {
495         }
496         return Base64.getEncoder().encodeToString(encryptedRaw);
497     }
498
499     public void sendOnvifRequest(RequestType requestType, String xAddr) {
500         logger.trace("Sending ONVIF request:{}", requestType);
501         String security = "";
502         String extraEnvelope = "";
503         String headerTo = "";
504         String getXmlCache = getXml(requestType);
505         if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
506                 || requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
507             headerTo = "<a:To s:mustUnderstand=\"1\">" + xAddr + "</a:To>";
508             extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
509         }
510         String headers;
511         if (!password.isEmpty() && !requestType.equals(RequestType.GetSystemDateAndTime)) {
512             String nonce = createNonce();
513             String dateTime = getUTCdateTime();
514             String digest = createDigest(nonce, dateTime);
515             security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
516                     + user
517                     + "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
518                     + digest
519                     + "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
520                     + encodeBase64(nonce)
521                     + "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
522                     + dateTime + "</Created></UsernameToken></Security>";
523             headers = "<s:Header>" + security + headerTo + "</s:Header>";
524         } else {// GetSystemDateAndTime must not be password protected as per spec.
525             headers = "";
526         }
527         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"),
528                 removeIPfromUrl(xAddr));
529         String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
530         request.headers().add("Content-Type",
531                 "application/soap+xml; charset=utf-8; action=\"" + actionString + "/" + requestType + "\"");
532         request.headers().add("Charset", "utf-8");
533         request.headers().set("Host", ipAddress + ":" + onvifPort);
534         request.headers().set("Connection", HttpHeaderValues.CLOSE);
535         request.headers().set("Accept-Encoding", "gzip, deflate");
536         String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
537                 + headers
538                 + "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
539                 + getXmlCache + "</s:Body></s:Envelope>";
540         request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
541         ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
542         request.headers().set("Content-Length", bbuf.readableBytes());
543         request.content().clear().writeBytes(bbuf);
544
545         Bootstrap localBootstap = bootstrap;
546         if (localBootstap == null) {
547             mainEventLoopGroup = new NioEventLoopGroup(2);
548             localBootstap = new Bootstrap();
549             localBootstap.group(mainEventLoopGroup);
550             localBootstap.channel(NioSocketChannel.class);
551             localBootstap.option(ChannelOption.SO_KEEPALIVE, true);
552             localBootstap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
553             localBootstap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
554             localBootstap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
555             localBootstap.option(ChannelOption.TCP_NODELAY, true);
556             localBootstap.handler(new ChannelInitializer<SocketChannel>() {
557
558                 @Override
559                 public void initChannel(SocketChannel socketChannel) throws Exception {
560                     socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(20, 20, 20));
561                     socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
562                     socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
563                 }
564             });
565             bootstrap = localBootstap;
566         }
567         if (!mainEventLoopGroup.isShuttingDown()) {
568             bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() {
569
570                 @Override
571                 public void operationComplete(@Nullable ChannelFuture future) {
572                     if (future == null) {
573                         return;
574                     }
575                     if (future.isSuccess()) {
576                         connectError = false;
577                         Channel ch = future.channel();
578                         ch.writeAndFlush(request);
579                     } else { // an error occured
580                         if (future.isDone() && !future.isCancelled()) {
581                             Throwable cause = future.cause();
582                             logger.trace("connect failed - cause {}", cause.getMessage());
583                             if (cause instanceof ConnectTimeoutException) {
584                                 logger.debug("Camera is not reachable on IP {}", ipAddress);
585                                 connectError = true;
586                             } else if ((cause instanceof ConnectException)
587                                     && cause.getMessage().contains("Connection refused")) {
588                                 logger.debug("Camera ONVIF port {} is refused.", onvifPort);
589                                 refusedError = true;
590                             }
591                         }
592                         if (isConnected) {
593                             disconnect();
594                         }
595                     }
596                 }
597             });
598         } else {
599             logger.debug("ONVIF message not sent as connection is shutting down");
600         }
601     }
602
603     OnvifConnection getHandle() {
604         return this;
605     }
606
607     void getIPandPortFromUrl(String url) {
608         int beginIndex = url.indexOf(":");
609         int endIndex = url.indexOf("/", beginIndex);
610         if (beginIndex >= 0 && endIndex == -1) {// 192.168.1.1:8080
611             ipAddress = url.substring(0, beginIndex);
612             onvifPort = Integer.parseInt(url.substring(beginIndex + 1));
613         } else if (beginIndex >= 0 && endIndex > beginIndex) {// 192.168.1.1:8080/foo/bar
614             ipAddress = url.substring(0, beginIndex);
615             onvifPort = Integer.parseInt(url.substring(beginIndex + 1, endIndex));
616         } else {// 192.168.1.1
617             ipAddress = url;
618             deviceXAddr = "http://" + ipAddress + "/onvif/device_service";
619             logger.debug("No Onvif Port found when parsing:{}", url);
620             return;
621         }
622         deviceXAddr = "http://" + ipAddress + ":" + onvifPort + "/onvif/device_service";
623     }
624
625     public void gotoPreset(int index) {
626         if (ptzDevice) {
627             if (index > 0) {// 0 is reserved for HOME as cameras seem to start at preset 1.
628                 if (presetTokens.isEmpty()) {
629                     logger.warn("Camera did not report any ONVIF preset locations, updating preset tokens now.");
630                     sendPTZRequest(RequestType.GetPresets);
631                 } else {
632                     presetTokenIndex = index - 1;
633                     sendPTZRequest(RequestType.GotoPreset);
634                 }
635             }
636         }
637     }
638
639     public void eventRecieved(String eventMessage) {
640         String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
641         if (topic.isEmpty()) {
642             sendOnvifRequest(RequestType.Renew, subscriptionXAddr);
643             return;
644         }
645         String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
646         String dataValue = Helper.fetchXML(eventMessage, "tt:Data", "Value=\"");
647         logger.debug("Onvif Event Topic:{}, Data:{}, Value:{}", topic, dataName, dataValue);
648         switch (topic) {
649             case "RuleEngine/CellMotionDetector/Motion":
650                 if ("true".equals(dataValue)) {
651                     ipCameraHandler.motionDetected(CHANNEL_CELL_MOTION_ALARM);
652                 } else if ("false".equals(dataValue)) {
653                     ipCameraHandler.noMotionDetected(CHANNEL_CELL_MOTION_ALARM);
654                 }
655                 break;
656             case "VideoSource/MotionAlarm":
657                 if ("true".equals(dataValue)) {
658                     ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
659                 } else if ("false".equals(dataValue)) {
660                     ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
661                 }
662                 break;
663             case "AudioAnalytics/Audio/DetectedSound":
664                 if ("true".equals(dataValue)) {
665                     ipCameraHandler.audioDetected();
666                 } else if ("false".equals(dataValue)) {
667                     ipCameraHandler.noAudioDetected();
668                 }
669                 break;
670             case "RuleEngine/FieldDetector/ObjectsInside":
671                 if ("true".equals(dataValue)) {
672                     ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
673                 } else if ("false".equals(dataValue)) {
674                     ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
675                 }
676                 break;
677             case "RuleEngine/LineDetector/Crossed":
678                 if ("ObjectId".equals(dataName)) {
679                     ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
680                 } else {
681                     ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
682                 }
683                 break;
684             case "RuleEngine/TamperDetector/Tamper":
685                 if ("true".equals(dataValue)) {
686                     ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.ON);
687                 } else if ("false".equals(dataValue)) {
688                     ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.OFF);
689                 }
690                 break;
691             case "Device/HardwareFailure/StorageFailure":
692                 if ("true".equals(dataValue)) {
693                     ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.ON);
694                 } else if ("false".equals(dataValue)) {
695                     ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.OFF);
696                 }
697                 break;
698             case "VideoSource/ImageTooDark/AnalyticsService":
699             case "VideoSource/ImageTooDark/ImagingService":
700             case "VideoSource/ImageTooDark/RecordingService":
701                 if ("true".equals(dataValue)) {
702                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON);
703                 } else if ("false".equals(dataValue)) {
704                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF);
705                 }
706                 break;
707             case "VideoSource/GlobalSceneChange/AnalyticsService":
708             case "VideoSource/GlobalSceneChange/ImagingService":
709             case "VideoSource/GlobalSceneChange/RecordingService":
710                 if ("true".equals(dataValue)) {
711                     ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON);
712                 } else if ("false".equals(dataValue)) {
713                     ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF);
714                 }
715                 break;
716             case "VideoSource/ImageTooBright/AnalyticsService":
717             case "VideoSource/ImageTooBright/ImagingService":
718             case "VideoSource/ImageTooBright/RecordingService":
719                 if ("true".equals(dataValue)) {
720                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.ON);
721                 } else if ("false".equals(dataValue)) {
722                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.OFF);
723                 }
724                 break;
725             case "VideoSource/ImageTooBlurry/AnalyticsService":
726             case "VideoSource/ImageTooBlurry/ImagingService":
727             case "VideoSource/ImageTooBlurry/RecordingService":
728                 if ("true".equals(dataValue)) {
729                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON);
730                 } else if ("false".equals(dataValue)) {
731                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF);
732                 }
733                 break;
734             case "RuleEngine/MyRuleDetector/Visitor":
735                 if ("true".equals(dataValue)) {
736                     ipCameraHandler.changeAlarmState(CHANNEL_DOORBELL, OnOffType.ON);
737                 } else if ("false".equals(dataValue)) {
738                     ipCameraHandler.changeAlarmState(CHANNEL_DOORBELL, OnOffType.OFF);
739                 }
740                 break;
741             case "RuleEngine/MyRuleDetector/VehicleDetect":
742                 if ("true".equals(dataValue)) {
743                     ipCameraHandler.changeAlarmState(CHANNEL_CAR_ALARM, OnOffType.ON);
744                 } else if ("false".equals(dataValue)) {
745                     ipCameraHandler.changeAlarmState(CHANNEL_CAR_ALARM, OnOffType.OFF);
746                 }
747                 break;
748             case "RuleEngine/MyRuleDetector/DogCatDetect":
749                 if ("true".equals(dataValue)) {
750                     ipCameraHandler.changeAlarmState(CHANNEL_ANIMAL_ALARM, OnOffType.ON);
751                 } else if ("false".equals(dataValue)) {
752                     ipCameraHandler.changeAlarmState(CHANNEL_ANIMAL_ALARM, OnOffType.OFF);
753                 }
754                 break;
755             case "RuleEngine/MyRuleDetector/FaceDetect":
756                 if ("true".equals(dataValue)) {
757                     ipCameraHandler.changeAlarmState(CHANNEL_FACE_DETECTED, OnOffType.ON);
758                 } else if ("false".equals(dataValue)) {
759                     ipCameraHandler.changeAlarmState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
760                 }
761                 break;
762             case "RuleEngine/MyRuleDetector/PeopleDetect":
763                 if ("true".equals(dataValue)) {
764                     ipCameraHandler.changeAlarmState(CHANNEL_HUMAN_ALARM, OnOffType.ON);
765                 } else if ("false".equals(dataValue)) {
766                     ipCameraHandler.changeAlarmState(CHANNEL_HUMAN_ALARM, OnOffType.OFF);
767                 }
768                 break;
769             default:
770                 logger.debug("Please report this camera has an un-implemented ONVIF event. Topic:{}", topic);
771         }
772         sendOnvifRequest(RequestType.Renew, subscriptionXAddr);
773     }
774
775     public boolean supportsPTZ() {
776         return ptzDevice;
777     }
778
779     public void getStatus() {
780         if (ptzDevice) {
781             sendPTZRequest(RequestType.GetStatus);
782         }
783     }
784
785     public Float getAbsolutePan() {
786         return currentPanPercentage;
787     }
788
789     public Float getAbsoluteTilt() {
790         return currentTiltPercentage;
791     }
792
793     public Float getAbsoluteZoom() {
794         return currentZoomPercentage;
795     }
796
797     public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
798         if (ptzDevice) {
799             currentPanPercentage = panValue;
800             currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
801         }
802     }
803
804     public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
805         if (ptzDevice) {
806             currentTiltPercentage = tiltValue;
807             currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
808         }
809     }
810
811     public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
812         if (ptzDevice) {
813             currentZoomPercentage = zoomValue;
814             currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
815         }
816     }
817
818     public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
819         if (ptzDevice) {
820             sendPTZRequest(RequestType.AbsoluteMove);
821         }
822     }
823
824     public void setSelectedMediaProfile(int mediaProfileIndex) {
825         this.mediaProfileIndex = mediaProfileIndex;
826     }
827
828     List<String> listOfResults(String message, String heading, String key) {
829         List<String> results = new LinkedList<>();
830         String temp = "";
831         for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
832             startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
833             if (startLookingFromIndex >= 0) {
834                 temp = Helper.fetchXML(message.substring(startLookingFromIndex), heading, key);
835                 if (!temp.isEmpty()) {
836                     logger.trace("String was found:{}", temp);
837                     results.add(temp);
838                 } else {
839                     return results;// key string must not exist so stop looking.
840                 }
841                 startLookingFromIndex += temp.length();
842             }
843         }
844         return results;
845     }
846
847     void parsePresets(String message) {
848         List<StateOption> presets = new ArrayList<>();
849         int counter = 1;// Presets start at 1 not 0. HOME may be added to index 0.
850         presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
851         presetNames = listOfResults(message, "<tptz:Preset", "<tt:Name>");
852         if (presetTokens.size() != presetNames.size()) {
853             logger.warn("Camera did not report the same number of Tokens and Names for PTZ presets");
854             return;
855         }
856         for (String value : presetNames) {
857             presets.add(new StateOption(Integer.toString(counter++), value));
858         }
859         ipCameraHandler.stateDescriptionProvider
860                 .setStateOptions(new ChannelUID(ipCameraHandler.getThing().getUID(), CHANNEL_GOTO_PRESET), presets);
861     }
862
863     void parseProfiles(String message) {
864         mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
865         if (mediaProfileIndex >= mediaProfileTokens.size()) {
866             logger.warn(
867                     "You have set the media profile to {} when the camera reported {} profiles. Falling back to mainstream 0.",
868                     mediaProfileIndex, mediaProfileTokens.size());
869             mediaProfileIndex = 0;
870         }
871     }
872
873     void processPTZLocation(String result) {
874         logger.debug("Processing new PTZ location now");
875
876         int beginIndex = result.indexOf("x=\"");
877         int endIndex = result.indexOf("\"", (beginIndex + 3));
878         if (beginIndex >= 0 && endIndex >= 0) {
879             currentPanCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
880             currentPanPercentage = (((panRangeMin - currentPanCamValue) * -1) / ((panRangeMin - panRangeMax) * -1))
881                     * 100;
882             logger.debug("Pan is updating to:{} and the cam value is {}", Math.round(currentPanPercentage),
883                     currentPanCamValue);
884         } else {
885             logger.warn(
886                     "Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
887             return;
888         }
889
890         beginIndex = result.indexOf("y=\"");
891         endIndex = result.indexOf("\"", (beginIndex + 3));
892         if (beginIndex >= 0 && endIndex >= 0) {
893             currentTiltCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
894             currentTiltPercentage = (((tiltRangeMin - currentTiltCamValue) * -1) / ((tiltRangeMin - tiltRangeMax) * -1))
895                     * 100;
896             logger.debug("Tilt is updating to:{} and the cam value is {}", Math.round(currentTiltPercentage),
897                     currentTiltCamValue);
898         } else {
899             return;
900         }
901
902         beginIndex = result.lastIndexOf("x=\"");
903         endIndex = result.indexOf("\"", (beginIndex + 3));
904         if (beginIndex >= 0 && endIndex >= 0) {
905             currentZoomCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
906             currentZoomPercentage = (((zoomMin - currentZoomCamValue) * -1) / ((zoomMin - zoomMax) * -1)) * 100;
907             logger.debug("Zoom is updating to:{} and the cam value is {}", Math.round(currentZoomPercentage),
908                     currentZoomCamValue);
909         } else {
910             return;
911         }
912     }
913
914     public void sendPTZRequest(RequestType requestType) {
915         if (!isConnected) {
916             logger.debug("ONVIF was not connected when a PTZ request was made, connecting now");
917             connect(usingEvents);
918         }
919         sendOnvifRequest(requestType, ptzXAddr);
920     }
921
922     public void sendEventRequest(RequestType requestType) {
923         sendOnvifRequest(requestType, eventXAddr);
924     }
925
926     public void connect(boolean useEvents) {
927         connecting.lock();
928         try {
929             if (!isConnected) {
930                 logger.debug("Connecting {} to ONVIF", ipAddress);
931                 threadPool = Executors.newScheduledThreadPool(2);
932                 sendOnvifRequest(RequestType.GetSystemDateAndTime, deviceXAddr);
933                 usingEvents = useEvents;
934                 sendOnvifRequest(RequestType.GetCapabilities, deviceXAddr);
935             }
936         } finally {
937             connecting.unlock();
938         }
939     }
940
941     public boolean isConnectError() {
942         return connectError;
943     }
944
945     public boolean isRefusedError() {
946         return refusedError;
947     }
948
949     public boolean isConnected() {
950         connecting.lock();
951         try {
952             return isConnected;
953         } finally {
954             connecting.unlock();
955         }
956     }
957
958     public void setIsConnected(boolean isConnected) {
959         connecting.lock();
960         try {
961             this.isConnected = isConnected;
962             this.connectError = false;
963             this.refusedError = false;
964         } finally {
965             connecting.unlock();
966         }
967     }
968
969     private void cleanup() {
970         if (!isConnected && !mainEventLoopGroup.isShuttingDown()) {
971             try {
972                 mainEventLoopGroup.shutdownGracefully();
973                 mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
974             } catch (InterruptedException e) {
975                 logger.warn("ONVIF was not cleanly shutdown, due to being interrupted");
976             } finally {
977                 logger.debug("Eventloop is shutdown:{}", mainEventLoopGroup.isShutdown());
978                 bootstrap = null;
979                 threadPool.shutdown();
980             }
981         }
982     }
983
984     public void disconnect() {
985         connecting.lock();// Lock out multiple disconnect()/connect() attempts as we try to send Unsubscribe.
986         try {
987             if (bootstrap != null) {
988                 if (isConnected && usingEvents && !mainEventLoopGroup.isShuttingDown()) {
989                     // Only makes sense to send if connected
990                     // Some cameras may continue to send events even when they can't reach a server.
991                     sendOnvifRequest(RequestType.Unsubscribe, subscriptionXAddr);
992                 }
993                 // give time for the Unsubscribe request to be sent, shutdownGracefully will try to send it first.
994                 threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS);
995             } else {
996                 cleanup();
997             }
998
999             isConnected = false;// isConnected is not thread safe, connecting.lock() used as fix.
1000         } finally {
1001             connecting.unlock();
1002         }
1003     }
1004 }