2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipcamera.internal.onvif;
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
17 import java.net.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;
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;
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;
65 * The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
69 * @author Matthew Skinner - Initial contribution
73 public class OnvifConnection {
74 public static enum RequestType {
84 CreatePullPointSubscription,
88 GetServiceCapabilities,
104 GetConfigurationOptions,
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;
135 // These hold the cameras PTZ position in the range that the camera uses, ie
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;
158 public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
159 this.ipCameraHandler = ipCameraHandler;
160 if (!ipAddress.isEmpty()) {
162 this.password = password;
163 getIPandPortFromUrl(ipAddress);
167 private String getXml(RequestType requestType) {
169 switch (requestType) {
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>";
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>";
217 case GetDeviceInformation:
218 return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
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>";
224 return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
225 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
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\"/>";
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>";
237 return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
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>";
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>";
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>";
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>"
279 + "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
281 return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
283 return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
284 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
286 return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
287 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
288 + presetTokens.get(presetTokenIndex) + "</PresetToken></GotoPreset>";
290 return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
291 + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
293 } catch (IndexOutOfBoundsException e) {
295 logger.debug("IndexOutOfBoundsException occured, camera is not connected via ONVIF: {}",
298 logger.debug("IndexOutOfBoundsException occured, {}", e.getMessage());
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.
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.
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));
323 sendPTZRequest(RequestType.GetNodes);
325 if (usingEvents) {// stops API cameras from getting sent ONVIF events.
326 sendOnvifRequest(requestBuilder(RequestType.GetEventProperties, eventXAddr));
327 sendOnvifRequest(requestBuilder(RequestType.GetServiceCapabilities, eventXAddr));
329 } else if (message.contains("GetServiceCapabilitiesResponse")) {
330 if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
331 sendOnvifRequest(requestBuilder(RequestType.Subscribe, eventXAddr));
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;
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;
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\"";
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>"
388 + "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
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.
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 + ">"
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);
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.
422 * @author Matthew Skinner - Initial contribution
424 String removeIPfromUrl(String url) {
425 int index = url.indexOf("//");
426 if (index != -1) {// now remove the :port
427 index = url.indexOf("/", index + 2);
430 logger.debug("We hit an issue parsing url:{}", url);
433 return url.substring(index);
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);
442 logger.debug("We hit an issue extracting IP:PORT from url:{}", url);
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()) {
451 logger.debug("deviceXAddr:{}", deviceXAddr);
453 temp = Helper.fetchXML(message, "<tt:Events", "tt:XAddr");
454 if (!temp.isEmpty()) {
455 subscriptionXAddr = eventXAddr = temp;
456 logger.debug("eventsXAddr:{}", eventXAddr);
458 temp = Helper.fetchXML(message, "<tt:Media", "tt:XAddr");
459 if (!temp.isEmpty()) {
461 logger.debug("mediaXAddr:{}", mediaXAddr);
464 ptzXAddr = Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr");
465 if (ptzXAddr.isEmpty()) {
467 logger.trace("Camera must not support PTZ, it failed to give a <tt:PTZ><tt:XAddr>:{}", message);
469 logger.debug("ptzXAddr:{}", ptzXAddr);
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);
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());
489 String createNonce() {
490 Random nonce = new Random();
491 return "" + nonce.nextInt();
494 String encodeBase64(String raw) {
495 return Base64.getEncoder().encodeToString(raw.getBytes());
498 String createDigest(String nOnce, String dateTime) {
499 String beforeEncryption = nOnce + dateTime + password;
500 MessageDigest msgDigest;
501 byte[] encryptedRaw = null;
503 msgDigest = MessageDigest.getInstance("SHA-1");
505 msgDigest.update(beforeEncryption.getBytes(StandardCharsets.UTF_8));
506 encryptedRaw = msgDigest.digest();
507 } catch (NoSuchAlgorithmException e) {
509 return Base64.getEncoder().encodeToString(encryptedRaw);
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>() {
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()));
533 bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() {
536 public void operationComplete(@Nullable ChannelFuture future) {
537 if (future == null) {
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);
553 OnvifConnection getHandle() {
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
568 logger.debug("No Onvif Port found when parsing:{}", url);
572 public void gotoPreset(int index) {
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);
579 presetTokenIndex = index - 1;
580 sendPTZRequest(RequestType.GotoPreset);
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);
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);
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);
608 case "AudioAnalytics/Audio/DetectedSound":
609 if ("true".equals(dataValue)) {
610 ipCameraHandler.audioDetected();
611 } else if ("false".equals(dataValue)) {
612 ipCameraHandler.noAudioDetected();
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);
622 case "RuleEngine/LineDetector/Crossed":
623 if ("ObjectId".equals(dataName)) {
624 ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
626 ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
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);
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);
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);
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);
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);
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);
681 sendOnvifRequest(requestBuilder(RequestType.Renew, subscriptionXAddr));
684 public boolean supportsPTZ() {
688 public void getStatus() {
690 sendPTZRequest(RequestType.GetStatus);
694 public Float getAbsolutePan() {
695 return currentPanPercentage;
698 public Float getAbsoluteTilt() {
699 return currentTiltPercentage;
702 public Float getAbsoluteZoom() {
703 return currentZoomPercentage;
706 public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
708 currentPanPercentage = panValue;
709 currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
713 public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
715 currentTiltPercentage = tiltValue;
716 currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
720 public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
722 currentZoomPercentage = zoomValue;
723 currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
727 public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
729 sendPTZRequest(RequestType.AbsoluteMove);
733 public void setSelectedMediaProfile(int mediaProfileIndex) {
734 this.mediaProfileIndex = mediaProfileIndex;
737 List<String> listOfResults(String message, String heading, String key) {
738 List<String> results = new LinkedList<>();
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);
748 return results;// key string must not exist so stop looking.
750 startLookingFromIndex += temp.length();
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");
765 for (String value : presetNames) {
766 presets.add(new StateOption(Integer.toString(counter++), value));
768 ipCameraHandler.stateDescriptionProvider
769 .setStateOptions(new ChannelUID(ipCameraHandler.getThing().getUID(), CHANNEL_GOTO_PRESET), presets);
772 void parseProfiles(String message) {
773 mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
774 if (mediaProfileIndex >= mediaProfileTokens.size()) {
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;
782 void processPTZLocation(String result) {
783 logger.debug("Processing new PTZ location now");
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))
791 logger.debug("Pan is updating to:{} and the cam value is {}", Math.round(currentPanPercentage),
795 "Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
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))
805 logger.debug("Tilt is updating to:{} and the cam value is {}", Math.round(currentTiltPercentage),
806 currentTiltCamValue);
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);
823 public void sendPTZRequest(RequestType requestType) {
825 logger.debug("ONVIF was not connected when a PTZ request was made, connecting now");
826 connect(usingEvents);
828 sendOnvifRequest(requestBuilder(requestType, ptzXAddr));
831 public void sendEventRequest(RequestType requestType) {
832 sendOnvifRequest(requestBuilder(requestType, eventXAddr));
835 public void connect(boolean useEvents) {
837 sendOnvifRequest(requestBuilder(RequestType.GetSystemDateAndTime, deviceXAddr));
838 usingEvents = useEvents;
842 public boolean isConnected() {
846 private void cleanup() {
847 mainEventLoopGroup.shutdownGracefully();
849 if (!mainEventLoopGroup.isShutdown()) {
851 mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
852 } catch (InterruptedException e) {
853 logger.warn("ONVIF was not cleanly shutdown, due to being interrupted");
855 logger.debug("Eventloop is shutdown:{}", mainEventLoopGroup.isShutdown());
859 threadPool.shutdown();
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));
868 // give time for the Unsubscribe request to be sent to the camera.
869 threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS);