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.luxtronikheatpump.internal.discovery;
15 import java.io.IOException;
16 import java.net.Inet4Address;
17 import java.net.InetAddress;
18 import java.net.UnknownHostException;
19 import java.util.List;
21 import java.util.concurrent.ExecutorService;
22 import java.util.concurrent.Executors;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.luxtronikheatpump.internal.ChannelUpdaterJob;
29 import org.openhab.binding.luxtronikheatpump.internal.HeatpumpConnector;
30 import org.openhab.binding.luxtronikheatpump.internal.LuxtronikHeatpumpBindingConstants;
31 import org.openhab.core.config.discovery.AbstractDiscoveryService;
32 import org.openhab.core.config.discovery.DiscoveryResult;
33 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
34 import org.openhab.core.config.discovery.DiscoveryService;
35 import org.openhab.core.net.CidrAddress;
36 import org.openhab.core.net.NetUtil;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.ThingUID;
39 import org.osgi.service.component.annotations.Component;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * Discovery class for Luxtronik heat pumps.
45 * As the heat pump seems undiscoverable using mdns or upnp we currently iterate over all
46 * IPs and send a socket request on port 8888 / 8889 and detect new heat pumps based on the results.
48 * @author Stefan Giehl - Initial contribution
51 @Component(service = { DiscoveryService.class,
52 LuxtronikHeatpumpDiscovery.class }, configurationPid = "discovery.luxtronik")
53 public class LuxtronikHeatpumpDiscovery extends AbstractDiscoveryService {
55 private final Logger logger = LoggerFactory.getLogger(LuxtronikHeatpumpDiscovery.class);
58 * HTTP read timeout (in milliseconds) - allows us to shutdown the listening every TIMEOUT
60 private static final int TIMEOUT_MS = 500;
63 * Timeout in seconds of the complete scan
65 private static final int FULL_SCAN_TIMEOUT_SECONDS = 30;
68 * Total number of concurrent threads during scanning.
70 private static final int SCAN_THREADS = 10;
73 * Whether we are currently scanning or not
75 private boolean scanning;
79 private int addressCount;
80 private @Nullable CidrAddress baseIp;
83 * The {@link ExecutorService} to run the listening threads on.
85 private @Nullable ExecutorService executorService;
88 * Constructs the discovery class using the thing IDs that we can discover.
90 public LuxtronikHeatpumpDiscovery() {
91 super(LuxtronikHeatpumpBindingConstants.SUPPORTED_THING_TYPES_UIDS, FULL_SCAN_TIMEOUT_SECONDS, false);
94 private void setupBaseIp(CidrAddress adr) {
95 byte[] octets = adr.getAddress().getAddress();
96 addressCount = (1 << (32 - adr.getPrefix())) - 2;
97 ipMask = 0xFFFFFFFF << (32 - adr.getPrefix());
98 octets[0] &= ipMask >> 24;
99 octets[1] &= ipMask >> 16;
100 octets[2] &= ipMask >> 8;
103 InetAddress iAdr = InetAddress.getByAddress(octets);
104 baseIp = new CidrAddress(iAdr, (short) adr.getPrefix());
105 } catch (UnknownHostException e) {
106 logger.debug("Could not build net ip address.", e);
111 private synchronized String getNextIPAddress(CidrAddress adr) {
114 byte[] octets = adr.getAddress().getAddress();
115 octets[2] += (octet >> 8);
119 InetAddress iAdr = null;
120 iAdr = InetAddress.getByAddress(octets);
121 address = iAdr.getHostAddress();
122 } catch (UnknownHostException e) {
123 logger.debug("Could not find next ip address.", e);
131 * Starts the scan. This discovery will:
133 * <li>Request this hosts first IPV4 address.</li>
134 * <li>Send a socket request on port 8888 / 8889 to all IPs on the subnet.</li>
135 * <li>The response is then investigated to see if is an answer from a heat pump</li>
137 * The process will continue until all addresses are checked, timeout or {@link #stopScan()} is called.
140 protected void startScan() {
141 if (executorService != null) {
145 CidrAddress localAdr = getLocalIP4Address();
146 if (localAdr == null) {
150 setupBaseIp(localAdr);
151 CidrAddress baseAdr = baseIp;
153 ExecutorService localExecutorService = Executors.newFixedThreadPool(SCAN_THREADS);
154 executorService = localExecutorService;
155 for (int i = 0; i < addressCount; i++) {
157 localExecutorService.execute(() -> {
158 if (scanning && baseAdr != null) {
159 String ipAdd = getNextIPAddress(baseAdr);
161 if (!discoverFromIp(ipAdd, 8889)) {
162 discoverFromIp(ipAdd, 8888);
169 private boolean discoverFromIp(String ipAdd, int port) {
170 HeatpumpConnector connection = new HeatpumpConnector(ipAdd, port);
174 Integer[] heatpumpValues = connection.getValues();
175 Map<String, Object> properties = ChannelUpdaterJob.getProperties(heatpumpValues);
176 properties.put("port", port);
178 String type = properties.get("heatpumpType").toString();
179 ThingTypeUID typeId = LuxtronikHeatpumpBindingConstants.THING_TYPE_HEATPUMP;
180 ThingUID uid = new ThingUID(typeId, type);
182 DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(type)
184 thingDiscovered(result);
187 } catch (IOException e) {
188 // no heatpump found on given ip / port
195 * Tries to find valid IP4 address.
197 * @return An IP4 address or null if none is found.
199 private @Nullable CidrAddress getLocalIP4Address() {
200 List<CidrAddress> adrList = NetUtil.getAllInterfaceAddresses().stream()
201 .filter(a -> a.getAddress() instanceof Inet4Address).collect(Collectors.toList());
203 for (CidrAddress adr : adrList) {
204 // Don't return a "fake" DHCP lease.
205 if (!adr.toString().startsWith("169.254.")) {
215 * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
216 * within {@link #TIMEOUT_MS) * {@link #SCAN_THREADS} time then shutdown the {@link #executorService}
219 protected synchronized void stopScan() {
221 ExecutorService localExecutorService = executorService;
222 if (localExecutorService != null) {
225 localExecutorService.awaitTermination(TIMEOUT_MS * SCAN_THREADS, TimeUnit.MILLISECONDS);
226 } catch (InterruptedException e) {
227 logger.debug("Stop scan interrupted.", e);
229 localExecutorService.shutdown();
230 executorService = null;