Skip to content

Commit

Permalink
Improve multicast implementation
Browse files Browse the repository at this point in the history
Fixes #14198

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
  • Loading branch information
jlaur committed Jan 10, 2023
1 parent 0304d74 commit 2f8a471
Showing 1 changed file with 132 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.InterfaceAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.IllformedLocaleException;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -69,7 +75,7 @@
* @author Karel Goderis - Initial contribution
* @author Kai Kreuzer - Fixed lifecycle issues
* @author Martin Lepsy - Added protocol information to support WiFi devices & some refactoring for HomeDevice
* @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN)
* @author Jacob Laursen - Fixed multicast and protocol support (Zigbee/LAN)
**/
@NonNullByDefault
public class MieleBridgeHandler extends BaseBridgeHandler {
Expand Down Expand Up @@ -308,117 +314,151 @@ private List<HomeDevice> getHomeDevices() throws MieleRpcException {
}

private Runnable eventListenerRunnable = () -> {
if (IP_PATTERN.matcher((String) getConfig().get(INTERFACE)).matches()) {
while (true) {
// Get the address that we are going to connect to.
InetAddress address1 = null;
InetAddress address2 = null;
try {
address1 = InetAddress.getByName(JSON_RPC_MULTICAST_IP1);
address2 = InetAddress.getByName(JSON_RPC_MULTICAST_IP2);
} catch (UnknownHostException e) {
logger.debug("An exception occurred while setting up the multicast receiver: '{}'", e.getMessage());
}
final int TIMEOUT_MILLIS = 100;
final int SLEEP_MILLIS = 500;

byte[] buf = new byte[256];
MulticastSocket clientSocket = null;
String interfaceIpAddress = (String) getConfig().get(INTERFACE);
if (!IP_PATTERN.matcher(interfaceIpAddress).matches()) {
logger.debug("Invalid IP address for the multicast interface: '{}'", interfaceIpAddress);
return;
}

while (true) {
try {
clientSocket = new MulticastSocket(JSON_RPC_PORT);
clientSocket.setSoTimeout(100);

clientSocket.setInterface(InetAddress.getByName((String) getConfig().get(INTERFACE)));
clientSocket.joinGroup(address1);
clientSocket.joinGroup(address2);

while (true) {
try {
buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
clientSocket.receive(packet);

String event = new String(packet.getData());
logger.debug("Received a multicast event '{}' from '{}:{}'", event, packet.getAddress(),
packet.getPort());

String[] parts = event.split("&");
String id = null, name = null, value = null;
for (String p : parts) {
String[] subparts = p.split("=");
switch (subparts[0]) {
case "property": {
name = subparts[1];
break;
}
case "value": {
value = subparts[1].strip().trim();
break;
}
case "id": {
id = subparts[1];
break;
}
}
}
// Get the address that we are going to connect to.
InetSocketAddress address1 = null;
InetSocketAddress address2 = null;
try {
address1 = new InetSocketAddress(InetAddress.getByName(JSON_RPC_MULTICAST_IP1), JSON_RPC_PORT);
address2 = new InetSocketAddress(InetAddress.getByName(JSON_RPC_MULTICAST_IP2), JSON_RPC_PORT);
} catch (UnknownHostException e) {
// This can only happen if the hardcoded literal IP addresses are invalid.
logger.debug("An exception occurred while setting up the multicast receiver: '{}'", e.getMessage());
return;
}

if (id == null || name == null || value == null) {
continue;
}
while (!Thread.currentThread().isInterrupted()) {
MulticastSocket clientSocket = null;
try {
clientSocket = new MulticastSocket(JSON_RPC_PORT);
clientSocket.setSoTimeout(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);

// In XGW 3000 firmware 2.03 this was changed from UID (hdm:ZigBee:0123456789abcdef#210)
// to serial number (001234567890)
FullyQualifiedApplianceIdentifier applianceIdentifier;
if (id.startsWith("hdm:")) {
applianceIdentifier = new FullyQualifiedApplianceIdentifier(id);
} else {
HomeDevice device = cachedHomeDevicesByRemoteUid.get(id);
if (device == null) {
logger.debug("Multicast event not handled as id {} is unknown.", id);
continue;
}
applianceIdentifier = device.getApplianceIdentifier();
while (!Thread.currentThread().isInterrupted()) {
try {
byte[] buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
clientSocket.receive(packet);

String event = new String(packet.getData(), packet.getOffset(), packet.getLength(),
StandardCharsets.ISO_8859_1);
logger.debug("Received a multicast event '{}' from '{}:{}'", event, packet.getAddress(),
packet.getPort());

String[] parts = event.split("&");
String id = null, name = null, value = null;
for (String p : parts) {
String[] subparts = p.split("=");
switch (subparts[0]) {
case "property": {
name = subparts[1];
break;
}
var deviceProperty = new DeviceProperty();
deviceProperty.Name = name;
deviceProperty.Value = value;
ApplianceStatusListener listener = applianceStatusListeners
.get(applianceIdentifier.getApplianceId());
if (listener != null) {
listener.onAppliancePropertyChanged(deviceProperty);
case "value": {
value = subparts[1];
break;
}
} catch (SocketTimeoutException e) {
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
logger.debug("Event listener has been interrupted.");
case "id": {
id = subparts[1];
break;
}
}
}
} catch (Exception ex) {
logger.debug("An exception occurred while receiving multicast packets: '{}'", ex.getMessage());
}

// restart the cycle with a clean slate
try {
if (clientSocket != null) {
clientSocket.leaveGroup(address1);
clientSocket.leaveGroup(address2);
if (id == null || name == null || value == null) {
continue;
}

// In XGW 3000 firmware 2.03 this was changed from UID (hdm:ZigBee:0123456789abcdef#210)
// to serial number (001234567890)
FullyQualifiedApplianceIdentifier applianceIdentifier;
if (id.startsWith("hdm:")) {
applianceIdentifier = new FullyQualifiedApplianceIdentifier(id);
} else {
HomeDevice device = cachedHomeDevicesByRemoteUid.get(id);
if (device == null) {
logger.debug("Multicast event not handled as id {} is unknown.", id);
continue;
}
applianceIdentifier = device.getApplianceIdentifier();
}
var deviceProperty = new DeviceProperty();
deviceProperty.Name = name;
deviceProperty.Value = value;
ApplianceStatusListener listener = applianceStatusListeners
.get(applianceIdentifier.getApplianceId());
if (listener != null) {
listener.onAppliancePropertyChanged(deviceProperty);
}
} catch (SocketTimeoutException e) {
try {
Thread.sleep(SLEEP_MILLIS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
logger.debug("Event listener has been interrupted.");
break;
}
} catch (IOException e) {
logger.debug("An exception occurred while leaving multicast group: '{}'", e.getMessage());
}
}
} catch (IOException e) {
logger.debug("An exception occurred while receiving multicast packets: '{}'", e.getMessage());
} finally {
// restart the cycle with a clean slate
try {
if (clientSocket != null) {
clientSocket.close();
clientSocket.leaveGroup(address1, null);
clientSocket.leaveGroup(address2, null);
}
} catch (IOException e) {
logger.debug("An exception occurred while leaving multicast group: '{}'", e.getMessage());
}
if (clientSocket != null) {
clientSocket.close();
}
}
} else {
logger.debug("Invalid IP address for the multicast interface: '{}'", getConfig().get(INTERFACE));
}
};

private @Nullable NetworkInterface getMulticastInterface(String interfaceIpAddress) throws SocketException {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();

@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;
}
}
}

return null;
}

public JsonElement invokeOperation(String applianceId, String modelID, String methodName) throws MieleRpcException {
if (getThing().getStatus() != ThingStatus.ONLINE) {
throw new MieleRpcException("Bridge is offline, operations can not be invoked");
Expand Down

0 comments on commit 2f8a471

Please sign in to comment.