[miele] Add auto-detection of network interface for multicast events (#19516)

* Improve multicast interface configuration

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen
2025-10-25 17:34:27 +02:00
committed by GitHub
parent 31f8b39fd0
commit 9f5b8e89ee
7 changed files with 67 additions and 97 deletions
+1 -2
View File
@@ -48,7 +48,6 @@ discovery.miele:removalGracePeriod=30
| Configuration Parameter | Description |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| ipAddress | Network address of the Miele@home gateway |
| interface | Network address of openHAB host interface where the binding will listen for multicast events coming from the Miele@home gateway. |
| userName | Name of a registered Miele@home user. |
| password | Password for the registered Miele@home user. |
| language | Language for state, program and phase texts. Leave blank for system language. |
@@ -423,7 +422,7 @@ See oven.
## things/miele.things
```java
Bridge miele:xgw3000:home [ipAddress="192.168.0.18", interface="192.168.0.5"] {
Bridge miele:xgw3000:home [ipAddress="192.168.0.18"] {
Things:
Thing fridgefreezer freezer [uid="00124b000424be44#2"]
Thing hood hood [uid="001d63fffe020685#210"]
@@ -119,7 +119,6 @@ public class MieleBindingConstants {
// Bridge config properties
public static final String HOST = "ipAddress";
public static final String INTERFACE = "interface";
public static final String USER_NAME = "userName";
public static final String PASSWORD = "password";
public static final String LANGUAGE = "language";
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2010-2025 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.miele.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link XGW3000Configuration} class contains fields mapping thing configuration parameters.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class XGW3000Configuration {
public String ipAddress = "";
public String userName = "";
public String password = "";
public String language = "";
}
@@ -12,10 +12,7 @@
*/
package org.openhab.binding.miele.internal.discovery;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -116,26 +113,8 @@ public class MieleMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
return null;
}
String ipAddress = addresses[0].getHostAddress();
Map<String, Object> properties = new HashMap<>(2);
properties.put(MieleBindingConstants.HOST, ipAddress);
Socket socket = null;
try {
socket = new Socket(addresses[0], 80);
InetAddress ourAddress = socket.getLocalAddress();
String interfaceIpAddress = ourAddress.getHostAddress();
socket.close();
properties.put(MieleBindingConstants.INTERFACE, interfaceIpAddress);
logger.debug("Discovered Miele@home gateway with IP address {} and interface IP address {}", ipAddress,
interfaceIpAddress);
} catch (IOException e) {
logger.warn("An exception occurred while connecting to the Miele Gateway: '{}'", e.getMessage());
return null;
}
return DiscoveryResultBuilder.create(uid).withProperties(properties)
return DiscoveryResultBuilder.create(uid)
.withProperty(MieleBindingConstants.HOST, addresses[0].getHostAddress())
.withRepresentationProperty(MieleBindingConstants.HOST).withLabel("@text/discovery.xgw3000.label")
.build();
}
@@ -16,9 +16,9 @@ import static org.openhab.binding.miele.internal.MieleBindingConstants.*;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
@@ -28,7 +28,6 @@ import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.IllformedLocaleException;
import java.util.Iterator;
import java.util.List;
@@ -42,7 +41,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -52,10 +50,10 @@ import org.openhab.binding.miele.internal.MieleGatewayCommunicationController;
import org.openhab.binding.miele.internal.api.dto.DeviceClassObject;
import org.openhab.binding.miele.internal.api.dto.DeviceProperty;
import org.openhab.binding.miele.internal.api.dto.HomeDevice;
import org.openhab.binding.miele.internal.config.XGW3000Configuration;
import org.openhab.binding.miele.internal.discovery.MieleApplianceDiscoveryService;
import org.openhab.binding.miele.internal.exceptions.MieleRpcException;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
@@ -85,9 +83,6 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_XGW3000);
private static final Pattern IP_PATTERN = Pattern
.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
private static final int POLLING_PERIOD_SECONDS = 15;
private static final int JSON_RPC_PORT = 2810;
private static final String JSON_RPC_MULTICAST_IP1 = "239.255.68.139";
@@ -101,6 +96,7 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
private final HttpClient httpClient;
private final Gson gson = new Gson();
private XGW3000Configuration config;
private @NonNullByDefault({}) MieleGatewayCommunicationController gatewayCommunication;
private Set<DiscoveryListener> discoveryListeners = ConcurrentHashMap.newKeySet();
@@ -115,18 +111,23 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
public MieleBridgeHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
// Default configuration
this.config = new XGW3000Configuration();
}
@Override
public void initialize() {
logger.debug("Initializing handler for bridge {}", getThing().getUID());
if (!validateConfig(getConfig())) {
config = getConfigAs(XGW3000Configuration.class);
if (!validateConfig()) {
return;
}
try {
gatewayCommunication = new MieleGatewayCommunicationController(httpClient, (String) getConfig().get(HOST));
gatewayCommunication = new MieleGatewayCommunicationController(httpClient, config.ipAddress);
} catch (URISyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
return;
@@ -142,30 +143,18 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
return Set.of(MieleApplianceDiscoveryService.class);
}
private boolean validateConfig(Configuration config) {
if (config.get(HOST) == null || ((String) config.get(HOST)).isBlank()) {
private boolean validateConfig() {
if (config.ipAddress.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.ip-address-not-set");
return false;
}
if (config.get(INTERFACE) == null || ((String) config.get(INTERFACE)).isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.ip-multicast-interface-not-set");
return false;
}
if (!IP_PATTERN.matcher((String) config.get(INTERFACE)).matches()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.invalid-ip-multicast-interface [\"" + config.get(INTERFACE)
+ "\"]");
return false;
}
String language = (String) config.get(LANGUAGE);
if (language != null && !language.isBlank()) {
if (!config.language.isBlank()) {
try {
new Locale.Builder().setLanguageTag(language).build();
new Locale.Builder().setLanguageTag(config.language).build();
} catch (IllformedLocaleException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.configuration-error.invalid-language [\"" + language + "\"]");
"@text/offline.configuration-error.invalid-language [\"" + config.language + "\"]");
return false;
}
}
@@ -175,12 +164,11 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
private Runnable pollingRunnable = new Runnable() {
@Override
public void run() {
String host = (String) getConfig().get(HOST);
try {
List<HomeDevice> homeDevices = getHomeDevices();
if (!lastBridgeConnectionState) {
logger.debug("Connection to Miele Gateway {} established.", host);
logger.debug("Connection to Miele Gateway {} established.", config.ipAddress);
lastBridgeConnectionState = true;
}
updateStatus(ThingStatus.ONLINE);
@@ -231,7 +219,7 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
message);
}
if (lastBridgeConnectionState) {
logger.debug("Connection to Miele Gateway {} lost.", host);
logger.debug("Connection to Miele Gateway {} lost.", config.ipAddress);
lastBridgeConnectionState = false;
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message);
@@ -324,12 +312,13 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
}
private Runnable eventListenerRunnable = () -> {
String interfaceIpAddress = (String) getConfig().get(INTERFACE);
if (!IP_PATTERN.matcher(interfaceIpAddress).matches()) {
logger.debug("Invalid IP address for the multicast interface: '{}'", interfaceIpAddress);
NetworkInterface networkInterface = getMulticastNetworkInterface(config.ipAddress);
if (networkInterface == null) {
return;
}
logger.debug("Using multicast interface {} for gateway {}", networkInterface.getName(), config.ipAddress);
// Get the address that we are going to connect to.
InetSocketAddress address1 = null;
InetSocketAddress address2 = null;
@@ -347,12 +336,6 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
try {
clientSocket = new MulticastSocket(JSON_RPC_PORT);
clientSocket.setSoTimeout(MULTICAST_TIMEOUT_MILLIS);
NetworkInterface networkInterface = getMulticastInterface(interfaceIpAddress);
if (networkInterface == null) {
logger.warn("Unable to find network interface for address {}", interfaceIpAddress);
return;
}
clientSocket.setNetworkInterface(networkInterface);
clientSocket.joinGroup(address1, null);
clientSocket.joinGroup(address2, null);
@@ -442,25 +425,19 @@ public class MieleBridgeHandler extends BaseBridgeHandler {
}
};
private @Nullable NetworkInterface getMulticastInterface(String interfaceIpAddress) throws SocketException {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
private @Nullable NetworkInterface getMulticastNetworkInterface(String gatewayIpAddress) {
try {
InetAddress gatewayAddress = InetAddress.getByName(gatewayIpAddress);
@Nullable
NetworkInterface networkInterface;
while (networkInterfaces.hasMoreElements()) {
networkInterface = networkInterfaces.nextElement();
if (networkInterface.isLoopback()) {
continue;
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
if (logger.isTraceEnabled()) {
logger.trace("Found interface address {} -> {}", interfaceAddress.toString(),
interfaceAddress.getAddress().toString());
}
if (interfaceAddress.getAddress().toString().endsWith("/" + interfaceIpAddress)) {
return networkInterface;
}
try (DatagramSocket socket = new DatagramSocket()) {
socket.connect(gatewayAddress, 80);
InetAddress localAddress = socket.getLocalAddress();
return NetworkInterface.getByInetAddress(localAddress);
}
} catch (UnknownHostException e) {
logger.warn("Invalid gateway '{}': {}", gatewayIpAddress, e.getMessage());
} catch (SocketException e) {
logger.warn("Failed to determine network interface for '{}': {}", gatewayIpAddress, e.getMessage());
}
return null;
@@ -45,8 +45,6 @@ thing-type.miele.xgw3000.description = The Miele bridge represents the Miele@hom
thing-type.config.miele.appliance.uid.label = ID
thing-type.config.miele.appliance.uid.description = Unique identifier for specific appliance on the gateway.
thing-type.config.miele.xgw3000.interface.label = Network Address of the Multicast Interface
thing-type.config.miele.xgw3000.interface.description = Network address of openHAB host interface where the binding will listen for multicast events coming from the Miele@home gateway.
thing-type.config.miele.xgw3000.ipAddress.label = Network Address
thing-type.config.miele.xgw3000.ipAddress.description = Network address of the Miele@home gateway.
thing-type.config.miele.xgw3000.language.label = Language
@@ -134,8 +132,6 @@ channel-type.miele.water-consumption.description = Water consumption by the curr
offline.configuration-error.bridge-missing = Bridge is missing
offline.configuration-error.ip-address-not-set = Cannot connect to the Miele gateway: host IP address is not set.
offline.configuration-error.ip-multicast-interface-not-set = Cannot connect to the Miele gateway: multicast interface is not set.
offline.configuration-error.invalid-ip-multicast-interface = Invalid IP address for the multicast interface: {0}
offline.configuration-error.invalid-language = Invalid language: {0}
offline.configuration-error.uid-not-set = Appliance ID is not set
@@ -22,24 +22,16 @@
<label>Network Address</label>
<description>Network address of the Miele@home gateway.</description>
</parameter>
<parameter name="interface" type="text" required="true">
<context>network-address</context>
<label>Network Address of the Multicast Interface</label>
<description>Network address of openHAB host interface where the binding will listen for multicast events coming
from the Miele@home gateway.</description>
</parameter>
<parameter name="userName" type="text" required="false">
<parameter name="userName" type="text">
<label>Username</label>
<description>
Name of a registered Miele@home user.
</description>
<description>Name of a registered Miele@home user.</description>
</parameter>
<parameter name="password" type="text" required="false">
<parameter name="password" type="text">
<context>password</context>
<label>Password</label>
<description>Password for the registered Miele@home user.</description>
</parameter>
<parameter name="language" type="text" required="false">
<parameter name="language" type="text">
<label>Language</label>
<description>Language for state, program and phase texts. Leave blank for system language.</description>
</parameter>