2 * Copyright (c) 2010-2023 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 java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.math.BigDecimal;
19 import java.net.HttpURLConnection;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.MalformedURLException;
23 import java.net.NetworkInterface;
24 import java.net.SocketException;
26 import java.net.UnknownHostException;
27 import java.nio.charset.StandardCharsets;
28 import java.util.ArrayList;
29 import java.util.Enumeration;
30 import java.util.List;
31 import java.util.UUID;
32 import java.util.concurrent.TimeUnit;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.openhab.binding.ipcamera.internal.Helper;
37 import org.openhab.binding.ipcamera.internal.IpCameraDiscoveryService;
38 import org.openhab.core.net.NetworkAddressService;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import io.netty.bootstrap.Bootstrap;
43 import io.netty.buffer.ByteBuf;
44 import io.netty.buffer.Unpooled;
45 import io.netty.channel.ChannelFactory;
46 import io.netty.channel.ChannelHandlerContext;
47 import io.netty.channel.ChannelOption;
48 import io.netty.channel.SimpleChannelInboundHandler;
49 import io.netty.channel.group.ChannelGroup;
50 import io.netty.channel.group.DefaultChannelGroup;
51 import io.netty.channel.nio.NioEventLoopGroup;
52 import io.netty.channel.socket.DatagramChannel;
53 import io.netty.channel.socket.DatagramPacket;
54 import io.netty.channel.socket.InternetProtocolFamily;
55 import io.netty.channel.socket.nio.NioDatagramChannel;
56 import io.netty.util.CharsetUtil;
57 import io.netty.util.concurrent.GlobalEventExecutor;
60 * The {@link OnvifDiscovery} is responsible for finding cameras that are ONVIF using UDP multicast.
62 * @author Matthew Skinner - Initial contribution
65 @io.netty.channel.ChannelHandler.Sharable
66 public class OnvifDiscovery {
67 private IpCameraDiscoveryService ipCameraDiscoveryService;
68 private final Logger logger = LoggerFactory.getLogger(OnvifDiscovery.class);
69 private final NetworkAddressService networkAddressService;
70 public ArrayList<DatagramPacket> listOfReplys = new ArrayList<DatagramPacket>(10);
72 public OnvifDiscovery(NetworkAddressService networkAddressService,
73 IpCameraDiscoveryService ipCameraDiscoveryService) {
74 this.ipCameraDiscoveryService = ipCameraDiscoveryService;
75 this.networkAddressService = networkAddressService;
78 public @Nullable List<NetworkInterface> getLocalNICs() {
79 String primaryHostAddress = networkAddressService.getPrimaryIpv4HostAddress();
80 List<NetworkInterface> results = new ArrayList<>(2);
82 for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
83 .hasMoreElements();) {
84 NetworkInterface networkInterface = enumNetworks.nextElement();
85 for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
86 .hasMoreElements();) {
87 InetAddress inetAddress = enumIpAddr.nextElement();
88 if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().length() < 18
89 && inetAddress.isSiteLocalAddress()) {
90 if (inetAddress.getHostAddress().equals(primaryHostAddress)) {
91 results.add(networkInterface);
92 logger.debug("Scanning network {} for any ONVIF cameras", primaryHostAddress);
94 logger.debug("Skipping network {} as it was not selected as openHAB's 'Primary Address'",
95 inetAddress.getHostAddress());
98 logger.debug("Skipping network {} as it was not site local", inetAddress.getHostAddress());
102 } catch (SocketException ex) {
107 void searchReply(String url, String xml) {
108 String ipAddress = "";
110 BigDecimal onvifPort = new BigDecimal(80);
112 logger.info("Camera found at xAddr: {}", url);
113 int endIndex = temp.indexOf(" ");// Some xAddr have two urls with a space in between.
115 temp = temp.substring(0, endIndex);// Use only the first url from now on.
118 int beginIndex = temp.indexOf(":") + 3;// add 3 to ignore the :// after http.
119 int secondIndex = temp.indexOf(":", beginIndex); // find second :
120 endIndex = temp.indexOf("/", beginIndex);
121 if (secondIndex > beginIndex && endIndex > secondIndex) {// http://192.168.0.1:8080/onvif/device_service
122 ipAddress = temp.substring(beginIndex, secondIndex);
123 onvifPort = new BigDecimal(temp.substring(secondIndex + 1, endIndex));
124 } else {// // http://192.168.0.1/onvif/device_service
125 ipAddress = temp.substring(beginIndex, endIndex);
127 String brand = checkForBrand(xml);
128 if ("onvif".equals(brand)) {
130 brand = getBrandFromLoginPage(ipAddress);
131 } catch (IOException e) {
135 ipCameraDiscoveryService.newCameraFound(brand, ipAddress, onvifPort.intValue());
138 void processCameraReplys() {
139 for (DatagramPacket packet : listOfReplys) {
140 String xml = packet.content().toString(CharsetUtil.UTF_8);
141 logger.trace("Device replied to discovery with: {}", xml);
142 String xAddr = Helper.fetchXML(xml, "", "d:XAddrs>");// Foscam <wsdd:XAddrs> and all other brands <d:XAddrs>
143 if (!xAddr.isEmpty()) {
144 searchReply(xAddr, xml);
145 } else if (xml.contains("onvif")) {
148 brand = getBrandFromLoginPage(packet.sender().getHostString());
149 } catch (IOException e) {
152 logger.debug("Possible {} camera found at: {}", brand, packet.sender().getHostString());
153 if ("reolink".equals(brand)) {
154 ipCameraDiscoveryService.newCameraFound(brand, packet.sender().getHostString(), 8000);
156 ipCameraDiscoveryService.newCameraFound(brand, packet.sender().getHostString(), 80);
162 String checkForBrand(String response) {
163 if (response.toLowerCase().contains("amcrest")) {
165 } else if (response.toLowerCase().contains("dahua")) {
167 } else if (response.toLowerCase().contains("doorbird")) {
169 } else if (response.toLowerCase().contains("foscam")) {
171 } else if (response.toLowerCase().contains("hikvision")) {
173 } else if (response.toLowerCase().contains("instar")) {
175 } else if (response.toLowerCase().contains("reolink")) {
177 } else if (response.toLowerCase().contains("ipc-")) {
179 } else if (response.toLowerCase().contains("dh-sd")) {
185 public String getBrandFromLoginPage(String hostname) throws IOException {
186 URL url = new URL("http://" + hostname);
187 String brand = "onvif";
188 HttpURLConnection connection = (HttpURLConnection) url.openConnection();
189 connection.setConnectTimeout(1000);
190 connection.setReadTimeout(2000);
191 connection.setInstanceFollowRedirects(true);
192 connection.setRequestMethod("GET");
194 connection.connect();
195 BufferedReader reply = new BufferedReader(new InputStreamReader(connection.getInputStream()));
196 String response = "";
198 while ((temp = reply.readLine()) != null) {
202 logger.trace("Cameras Login page is: {}", response);
203 brand = checkForBrand(response);
204 } catch (MalformedURLException e) {
206 connection.disconnect();
211 private DatagramPacket wsDiscovery() throws UnknownHostException {
212 String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"><e:Header><w:MessageID>uuid:"
214 + "</w:MessageID><w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To><w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action></e:Header><e:Body><d:Probe><d:Types xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
215 ByteBuf discoveryProbeMessage = Unpooled.copiedBuffer(xml, 0, xml.length(), StandardCharsets.UTF_8);
216 return new DatagramPacket(discoveryProbeMessage,
217 new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), new InetSocketAddress(0));
220 public void discoverCameras() throws UnknownHostException, InterruptedException {
221 List<NetworkInterface> nics = getLocalNICs();
222 if (nics == null || nics.isEmpty()) {
224 "No 'Primary Address' selected to use for camera discovery. Check openHAB's Network Settings page to select a valid Primary Address.");
227 NetworkInterface networkInterface = nics.get(0);
228 Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
229 .channelFactory(new ChannelFactory<NioDatagramChannel>() {
231 public NioDatagramChannel newChannel() {
232 return new NioDatagramChannel(InternetProtocolFamily.IPv4);
234 }).handler(new SimpleChannelInboundHandler<DatagramPacket>() {
236 protected void channelRead0(@Nullable ChannelHandlerContext ctx, DatagramPacket msg)
239 listOfReplys.add(msg);
241 }).option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, true)
242 .option(ChannelOption.IP_MULTICAST_LOOP_DISABLED, false).option(ChannelOption.SO_RCVBUF, 2048)
243 .option(ChannelOption.IP_MULTICAST_TTL, 255).option(ChannelOption.IP_MULTICAST_IF, networkInterface);
244 ChannelGroup openChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
245 for (NetworkInterface nic : nics) {
246 DatagramChannel datagramChannel = (DatagramChannel) bootstrap.option(ChannelOption.IP_MULTICAST_IF, nic)
247 .bind(new InetSocketAddress(0)).sync().channel();
249 .joinGroup(new InetSocketAddress(InetAddress.getByName("239.255.255.250"), 3702), networkInterface)
251 openChannels.add(datagramChannel);
253 if (!openChannels.isEmpty()) {
254 openChannels.writeAndFlush(wsDiscovery());
255 TimeUnit.SECONDS.sleep(6);
256 openChannels.close();
257 processCameraReplys();
258 bootstrap.config().group().shutdownGracefully();