mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[LuxtronikHeatpump] Adds discovery service (#11907)
* [LuxtronikHeatpump] Adds discovery service Signed-off-by: Stefan Giehl <stefangiehl@gmail.com>
This commit is contained in:
parent
8b4607a4f1
commit
c41c38405e
@ -23,6 +23,10 @@ Note: The whole functionality is based on data that was reverse engineered, so u
|
||||
|
||||
This binding only supports one thing type "Luxtronik Heatpump" (heatpump).
|
||||
|
||||
## Discovery
|
||||
|
||||
This binding will try to detect heat pumps that are reachable in the same IPv4 subnet.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
Each heatpump requires the following configuration parameters:
|
||||
|
@ -16,6 +16,8 @@ import java.io.IOException;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
@ -179,7 +181,7 @@ public class ChannelUpdaterJob implements SchedulerRunnable, Runnable {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
private String getSoftwareVersion(Integer[] heatpumpValues) {
|
||||
private static String getSoftwareVersion(Integer[] heatpumpValues) {
|
||||
StringBuffer softwareVersion = new StringBuffer("");
|
||||
|
||||
for (int i = 81; i <= 90; i++) {
|
||||
@ -191,7 +193,7 @@ public class ChannelUpdaterJob implements SchedulerRunnable, Runnable {
|
||||
return softwareVersion.toString();
|
||||
}
|
||||
|
||||
private String transformIpAddress(int ip) {
|
||||
private static String transformIpAddress(int ip) {
|
||||
return String.format("%d.%d.%d.%d", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);
|
||||
}
|
||||
|
||||
@ -225,20 +227,32 @@ public class ChannelUpdaterJob implements SchedulerRunnable, Runnable {
|
||||
handleEventType(new StringType(longState), HeatpumpChannel.CHANNEL_HEATPUMP_STATUS);
|
||||
}
|
||||
|
||||
private void updateProperties(Integer[] heatpumpValues) {
|
||||
public static Map<String, Object> getProperties(Integer[] heatpumpValues) {
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
|
||||
String heatpumpType = HeatpumpType.fromCode(heatpumpValues[78]).getName();
|
||||
|
||||
setProperty("heatpumpType", heatpumpType);
|
||||
properties.put("heatpumpType", heatpumpType);
|
||||
|
||||
// Not sure when Typ 2 should be used
|
||||
// String heatpumpType2 = HeatpumpType.fromCode(heatpumpValues[230]).getName();
|
||||
// setProperty("heatpumpType2", heatpumpType2);
|
||||
// properties.put("heatpumpType2", heatpumpType2);
|
||||
|
||||
setProperty("softwareVersion", getSoftwareVersion(heatpumpValues));
|
||||
setProperty("ipAddress", transformIpAddress(heatpumpValues[91]));
|
||||
setProperty("subnetMask", transformIpAddress(heatpumpValues[92]));
|
||||
setProperty("broadcastAddress", transformIpAddress(heatpumpValues[93]));
|
||||
setProperty("gateway", transformIpAddress(heatpumpValues[94]));
|
||||
properties.put("softwareVersion", getSoftwareVersion(heatpumpValues));
|
||||
properties.put("ipAddress", transformIpAddress(heatpumpValues[91]));
|
||||
properties.put("subnetMask", transformIpAddress(heatpumpValues[92]));
|
||||
properties.put("broadcastAddress", transformIpAddress(heatpumpValues[93]));
|
||||
properties.put("gateway", transformIpAddress(heatpumpValues[94]));
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private void updateProperties(Integer[] heatpumpValues) {
|
||||
Map<String, Object> properties = getProperties(heatpumpValues);
|
||||
|
||||
for (Map.Entry<String, Object> property : properties.entrySet()) {
|
||||
handler.updateProperty(property.getKey(), property.getValue().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private String getStateTranslation(String name, @Nullable Integer option) {
|
||||
@ -251,10 +265,6 @@ public class ChannelUpdaterJob implements SchedulerRunnable, Runnable {
|
||||
return translation == null ? "" : translation;
|
||||
}
|
||||
|
||||
private void setProperty(String name, String value) {
|
||||
handler.updateProperty(name, value);
|
||||
}
|
||||
|
||||
private String formatHours(@Nullable Integer value) {
|
||||
String returnValue = "";
|
||||
|
||||
|
@ -12,6 +12,9 @@
|
||||
*/
|
||||
package org.openhab.binding.luxtronikheatpump.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
@ -27,4 +30,6 @@ public class LuxtronikHeatpumpBindingConstants {
|
||||
public static final String BINDING_ID = "luxtronikheatpump";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_HEATPUMP = new ThingTypeUID(BINDING_ID, "heatpump");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_HEATPUMP);
|
||||
}
|
||||
|
@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.luxtronikheatpump.internal.discovery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.luxtronikheatpump.internal.ChannelUpdaterJob;
|
||||
import org.openhab.binding.luxtronikheatpump.internal.HeatpumpConnector;
|
||||
import org.openhab.binding.luxtronikheatpump.internal.LuxtronikHeatpumpBindingConstants;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.net.CidrAddress;
|
||||
import org.openhab.core.net.NetUtil;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Discovery class for Luxtronik heat pumps.
|
||||
* As the heat pump seems undiscoverable using mdns or upnp we currently iterate over all
|
||||
* IPs and send a socket request on port 8888 / 8889 and detect new heat pumps based on the results.
|
||||
*
|
||||
* @author Stefan Giehl - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { DiscoveryService.class,
|
||||
LuxtronikHeatpumpDiscovery.class }, configurationPid = "discovery.luxtronik")
|
||||
public class LuxtronikHeatpumpDiscovery extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LuxtronikHeatpumpDiscovery.class);
|
||||
|
||||
/**
|
||||
* HTTP read timeout (in milliseconds) - allows us to shutdown the listening every TIMEOUT
|
||||
*/
|
||||
private static final int TIMEOUT_MS = 500;
|
||||
|
||||
/**
|
||||
* Timeout in seconds of the complete scan
|
||||
*/
|
||||
private static final int FULL_SCAN_TIMEOUT_SECONDS = 30;
|
||||
|
||||
/**
|
||||
* Total number of concurrent threads during scanning.
|
||||
*/
|
||||
private static final int SCAN_THREADS = 10;
|
||||
|
||||
/**
|
||||
* Whether we are currently scanning or not
|
||||
*/
|
||||
private boolean scanning;
|
||||
|
||||
private int octet;
|
||||
private int ipMask;
|
||||
private int addressCount;
|
||||
private @Nullable CidrAddress baseIp;
|
||||
|
||||
/**
|
||||
* The {@link ExecutorService} to run the listening threads on.
|
||||
*/
|
||||
private @Nullable ExecutorService executorService;
|
||||
|
||||
/**
|
||||
* Constructs the discovery class using the thing IDs that we can discover.
|
||||
*/
|
||||
public LuxtronikHeatpumpDiscovery() {
|
||||
super(LuxtronikHeatpumpBindingConstants.SUPPORTED_THING_TYPES_UIDS, FULL_SCAN_TIMEOUT_SECONDS, false);
|
||||
}
|
||||
|
||||
private void setupBaseIp(CidrAddress adr) {
|
||||
byte[] octets = adr.getAddress().getAddress();
|
||||
addressCount = (1 << (32 - adr.getPrefix())) - 2;
|
||||
ipMask = 0xFFFFFFFF << (32 - adr.getPrefix());
|
||||
octets[0] &= ipMask >> 24;
|
||||
octets[1] &= ipMask >> 16;
|
||||
octets[2] &= ipMask >> 8;
|
||||
octets[3] &= ipMask;
|
||||
try {
|
||||
InetAddress iAdr = InetAddress.getByAddress(octets);
|
||||
baseIp = new CidrAddress(iAdr, (short) adr.getPrefix());
|
||||
} catch (UnknownHostException e) {
|
||||
logger.debug("Could not build net ip address.", e);
|
||||
}
|
||||
octet = 0;
|
||||
}
|
||||
|
||||
private synchronized String getNextIPAddress(CidrAddress adr) {
|
||||
octet++;
|
||||
octet &= ~ipMask;
|
||||
byte[] octets = adr.getAddress().getAddress();
|
||||
octets[2] += (octet >> 8);
|
||||
octets[3] += octet;
|
||||
String address = "";
|
||||
try {
|
||||
InetAddress iAdr = null;
|
||||
iAdr = InetAddress.getByAddress(octets);
|
||||
address = iAdr.getHostAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
logger.debug("Could not find next ip address.", e);
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Starts the scan. This discovery will:
|
||||
* <ul>
|
||||
* <li>Request this hosts first IPV4 address.</li>
|
||||
* <li>Send a socket request on port 8888 / 8889 to all IPs on the subnet.</li>
|
||||
* <li>The response is then investigated to see if is an answer from a heat pump</li>
|
||||
* </ul>
|
||||
* The process will continue until all addresses are checked, timeout or {@link #stopScan()} is called.
|
||||
*/
|
||||
@Override
|
||||
protected void startScan() {
|
||||
if (executorService != null) {
|
||||
stopScan();
|
||||
}
|
||||
|
||||
CidrAddress localAdr = getLocalIP4Address();
|
||||
if (localAdr == null) {
|
||||
stopScan();
|
||||
return;
|
||||
}
|
||||
setupBaseIp(localAdr);
|
||||
CidrAddress baseAdr = baseIp;
|
||||
scanning = true;
|
||||
ExecutorService localExecutorService = Executors.newFixedThreadPool(SCAN_THREADS);
|
||||
executorService = localExecutorService;
|
||||
for (int i = 0; i < addressCount; i++) {
|
||||
|
||||
localExecutorService.execute(() -> {
|
||||
if (scanning && baseAdr != null) {
|
||||
String ipAdd = getNextIPAddress(baseAdr);
|
||||
|
||||
if (!discoverFromIp(ipAdd, 8889)) {
|
||||
discoverFromIp(ipAdd, 8888);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private boolean discoverFromIp(String ipAdd, int port) {
|
||||
HeatpumpConnector connection = new HeatpumpConnector(ipAdd, port);
|
||||
|
||||
try {
|
||||
connection.read();
|
||||
Integer[] heatpumpValues = connection.getValues();
|
||||
Map<String, Object> properties = ChannelUpdaterJob.getProperties(heatpumpValues);
|
||||
properties.put("port", port);
|
||||
|
||||
String type = properties.get("heatpumpType").toString();
|
||||
ThingTypeUID typeId = LuxtronikHeatpumpBindingConstants.THING_TYPE_HEATPUMP;
|
||||
ThingUID uid = new ThingUID(typeId, type);
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(type)
|
||||
.build();
|
||||
thingDiscovered(result);
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
// no heatpump found on given ip / port
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find valid IP4 address.
|
||||
*
|
||||
* @return An IP4 address or null if none is found.
|
||||
*/
|
||||
private @Nullable CidrAddress getLocalIP4Address() {
|
||||
List<CidrAddress> adrList = NetUtil.getAllInterfaceAddresses().stream()
|
||||
.filter(a -> a.getAddress() instanceof Inet4Address).collect(Collectors.toList());
|
||||
|
||||
for (CidrAddress adr : adrList) {
|
||||
// Don't return a "fake" DHCP lease.
|
||||
if (!adr.toString().startsWith("169.254.")) {
|
||||
return adr;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
|
||||
* within {@link #TIMEOUT_MS) * {@link #SCAN_THREADS} time then shutdown the {@link #executorService}
|
||||
*/
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
super.stopScan();
|
||||
ExecutorService localExecutorService = executorService;
|
||||
if (localExecutorService != null) {
|
||||
scanning = false;
|
||||
try {
|
||||
localExecutorService.awaitTermination(TIMEOUT_MS * SCAN_THREADS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
logger.debug("Stop scan interrupted.", e);
|
||||
}
|
||||
localExecutorService.shutdown();
|
||||
executorService = null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user