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