diff --git a/bundles/org.openhab.binding.atlona/README.md b/bundles/org.openhab.binding.atlona/README.md index 121563ebe0c..9330fbb3e4e 100644 --- a/bundles/org.openhab.binding.atlona/README.md +++ b/bundles/org.openhab.binding.atlona/README.md @@ -17,9 +17,9 @@ This binding supports the following thing types: ## Discovery -The Atlona AT-UHD-PRO3 switch can be discovered by starting a discovery scan in the UI and then logging into your switch and pressing the "SDDP" button on the "Network" tab. -The "SDDP" (simple device discovery protocol) button will initiate the discovery process. -If "Telnet Login" is enabled ("Network" tab from the switch configuration UI), you will need to set the username and password in the configuration of the newly discovered thing before a connection can be made. +Supported things should be discovered automatically upon receipt of periodic SDDP announcements from the switch. +If the thing is not discovered automatically, login to the switch configuration UI and press the "SDDP" button on the "Network" tab to force the switch to send the SDDP announcement. +If "Telnet Login" is enabled in the switch configuration, you will need to set the username and password in the newly discovered thing before a connection can be made. ## Thing Configuration diff --git a/bundles/org.openhab.binding.atlona/src/main/feature/feature.xml b/bundles/org.openhab.binding.atlona/src/main/feature/feature.xml index 729fcb94d64..ea775b169e5 100644 --- a/bundles/org.openhab.binding.atlona/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.atlona/src/main/feature/feature.xml @@ -4,6 +4,7 @@ openhab-runtime-base + openhab-core-config-discovery-sddp mvn:org.openhab.addons.bundles/org.openhab.binding.atlona/${project.version} diff --git a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscovery.java b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscovery.java deleted file mode 100644 index 3e9c80660db..00000000000 --- a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscovery.java +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Copyright (c) 2010-2024 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.atlona.internal.discovery; - -import static org.openhab.binding.atlona.internal.AtlonaBindingConstants.*; - -import java.io.IOException; -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.MulticastSocket; -import java.net.NetworkInterface; -import java.net.SocketTimeoutException; -import java.util.Collections; -import java.util.HashMap; -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 java.util.stream.Stream; - -import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Config; -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.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 the Atlona PRO3 line. The PRO3 line uses SDDP (simple device discovery protocol) for discovery - * (similar to UPNP but defined by Control4). The user should start the discovery process in openhab and then log into - * the switch, go to the Network options and press the SDDP button (which initiates the SDDP conversation). - * - * @author Tim Roberts - Initial contribution - */ -@Component(service = DiscoveryService.class, configurationPid = "discovery.atlona") -public class AtlonaDiscovery extends AbstractDiscoveryService { - - private final Logger logger = LoggerFactory.getLogger(AtlonaDiscovery.class); - - /** - * Address SDDP broadcasts on - */ - private static final String SDDP_ADDR = "239.255.255.250"; - - /** - * Port number SDDP uses - */ - private static final int SDDP_PORT = 1902; - - /** - * SDDP packet should be only 512 in size - make it 600 to give us some room - */ - private static final int BUFFER_SIZE = 600; - - /** - * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT - */ - private static final int TIMEOUT = 1000; - - /** - * Whether we are currently scanning or not - */ - private boolean scanning; - - /** - * The {@link ExecutorService} to run the listening threads on. - */ - private ExecutorService executorService; - - /** - * Constructs the discovery class using the thing IDs that we can discover. - */ - public AtlonaDiscovery() { - super(Collections.unmodifiableSet( - Stream.of(THING_TYPE_PRO3_44M, THING_TYPE_PRO3_66M, THING_TYPE_PRO3_88M, THING_TYPE_PRO3_1616M) - .collect(Collectors.toSet())), - 30, false); - } - - /** - * {@inheritDoc} - * - * Starts the scan. This discovery will: - * - * The process will continue until {@link #stopScan()} is called. - */ - @Override - protected void startScan() { - if (executorService != null) { - stopScan(); - } - - logger.debug("Starting Discovery"); - - try { - final InetAddress addr = InetAddress.getByName(SDDP_ADDR); - final List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); - - executorService = Executors.newFixedThreadPool(networkInterfaces.size()); - scanning = true; - for (final NetworkInterface netint : networkInterfaces) { - - executorService.execute(() -> { - try { - MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT); - multiSocket.setSoTimeout(TIMEOUT); - multiSocket.setNetworkInterface(netint); - multiSocket.joinGroup(addr); - - while (scanning) { - DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE); - try { - multiSocket.receive(receivePacket); - - String message = new String(receivePacket.getData()).trim(); - if (message.length() > 0) { - messageReceive(message); - } - } catch (SocketTimeoutException e) { - // ignore - } - } - - multiSocket.close(); - } catch (Exception e) { - if (!e.getMessage().contains("No IP addresses bound to interface")) { - logger.debug("Error getting ip addresses: {}", e.getMessage(), e); - } - } - }); - } - } catch (IOException e) { - logger.debug("Error getting ip addresses: {}", e.getMessage(), e); - } - } - - /** - * SDDP message has the following format - * - *
-     * NOTIFY ALIVE SDDP/1.0
-     * From: "192.168.1.30:1902"
-     * Host: "AT-UHD-PRO3-88M_B898B0030F4D"
-     * Type: "AT-UHD-PRO3-88M"
-     * Max-Age: 1800
-     * Primary-Proxy: "avswitch"
-     * Proxies: "avswitch"
-     * Manufacturer: "Atlona"
-     * Model: "AT-UHD-PRO3-88M"
-     * Driver: "avswitch_Atlona_AT-UHD-PRO3-88M_IP.c4i"
-     * Config-URL: "http://192.168.1.30/"
-     * 
- * - * First parse the manufacturer, host, model and IP address from the message. For the "Host" field, we parse out the - * serial #. For the From field, we parse out the IP address (minus the port #). If we successfully found all four - * and the manufacturer is "Atlona" and it's a model we recognize, we then create our thing from it. - * - * @param message possibly null, possibly empty SDDP message - */ - private void messageReceive(String message) { - if (message == null || message.trim().length() == 0) { - return; - } - - String host = null; - String model = null; - String from = null; - String manufacturer = null; - - for (String msg : message.split("\r\n")) { - int idx = msg.indexOf(':'); - if (idx > 0) { - String name = msg.substring(0, idx); - - if ("Host".equalsIgnoreCase(name)) { - host = msg.substring(idx + 1).trim().replace("\"", ""); - int sep = host.indexOf('_'); - if (sep >= 0) { - host = host.substring(sep + 1); - } - } else if ("Model".equalsIgnoreCase(name)) { - model = msg.substring(idx + 1).trim().replace("\"", ""); - } else if ("Manufacturer".equalsIgnoreCase(name)) { - manufacturer = msg.substring(idx + 1).trim().replace("\"", ""); - } else if ("From".equalsIgnoreCase(name)) { - from = msg.substring(idx + 1).trim().replace("\"", ""); - int sep = from.indexOf(':'); - if (sep >= 0) { - from = from.substring(0, sep); - } - } - } - - } - - if (!"Atlona".equalsIgnoreCase(manufacturer)) { - return; - } - - if (host != null && model != null && from != null) { - ThingTypeUID typeId = null; - if ("AT-UHD-PRO3-44M".equalsIgnoreCase(model)) { - typeId = THING_TYPE_PRO3_44M; - } else if ("AT-UHD-PRO3-66M".equalsIgnoreCase(model)) { - typeId = THING_TYPE_PRO3_66M; - } else if ("AT-UHD-PRO3-88M".equalsIgnoreCase(model)) { - typeId = THING_TYPE_PRO3_88M; - } else if ("AT-UHD-PRO3-1616M".equalsIgnoreCase(model)) { - typeId = THING_TYPE_PRO3_1616M; - } else { - logger.warn("Unknown model #: {}", model); - } - - if (typeId != null) { - logger.debug("Creating binding for {} ({})", model, from); - ThingUID j = new ThingUID(typeId, host); - - Map properties = new HashMap<>(1); - properties.put(AtlonaPro3Config.IP_ADDRESS, from); - DiscoveryResult result = DiscoveryResultBuilder.create(j).withProperties(properties) - .withLabel(model + " (" + from + ")").build(); - thingDiscovered(result); - } - } - } - - /** - * {@inheritDoc} - * - * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally - * within {@link #TIMEOUT} * 5 time then shutdown the {@link ExecutorService} - */ - @Override - protected synchronized void stopScan() { - super.stopScan(); - if (executorService == null) { - return; - } - - scanning = false; - - try { - executorService.awaitTermination(TIMEOUT * 5, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - } - executorService.shutdown(); - executorService = null; - } -} diff --git a/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscoveryParticipant.java b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscoveryParticipant.java new file mode 100644 index 00000000000..144d6f8cb7d --- /dev/null +++ b/bundles/org.openhab.binding.atlona/src/main/java/org/openhab/binding/atlona/internal/discovery/AtlonaDiscoveryParticipant.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2024 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.atlona.internal.discovery; + +import static org.openhab.binding.atlona.internal.AtlonaBindingConstants.*; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.atlona.internal.pro3.AtlonaPro3Config; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.sddp.SddpDevice; +import org.openhab.core.config.discovery.sddp.SddpDiscoveryParticipant; +import org.openhab.core.thing.Thing; +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 Service for Atlona HDMI matrices that support SDDP. + * + * @author Michael Lobstein - Initial contribution + * + */ +@NonNullByDefault +@Component(immediate = true) +public class AtlonaDiscoveryParticipant implements SddpDiscoveryParticipant { + private final Logger logger = LoggerFactory.getLogger(AtlonaDiscoveryParticipant.class); + + private static final String ATLONA = "ATLONA"; + private static final String PROXY_AVSWITCH = "avswitch"; + + @Override + public Set getSupportedThingTypeUIDs() { + return Set.of(THING_TYPE_PRO3_44M, THING_TYPE_PRO3_66M, THING_TYPE_PRO3_88M, THING_TYPE_PRO3_1616M, + THING_TYPE_PRO3HD_44M, THING_TYPE_PRO3HD_66M); + } + + @Override + public @Nullable DiscoveryResult createResult(SddpDevice device) { + final ThingUID uid = getThingUID(device); + if (uid != null) { + final Map properties = new HashMap<>(2); + final String label = device.model + " (" + device.ipAddress + ")"; + + properties.put(Thing.PROPERTY_MAC_ADDRESS, device.macAddress); + properties.put(AtlonaPro3Config.IP_ADDRESS, device.ipAddress); + + final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(label).build(); + + logger.debug("Created a DiscoveryResult for device '{}' with UID '{}'", label, uid.getId()); + return result; + } else { + return null; + } + } + + /* + * The Atlona SDDP message has the following format + * + *
+     * NOTIFY ALIVE SDDP/1.0
+     * From: "192.168.1.30:1902"
+     * Host: "AT-UHD-PRO3-88M_B898B0030F4D"
+     * Type: "AT-UHD-PRO3-88M"
+     * Max-Age: 1800
+     * Primary-Proxy: "avswitch"
+     * Proxies: "avswitch"
+     * Manufacturer: "Atlona"
+     * Model: "AT-UHD-PRO3-88M"
+     * Driver: "avswitch_Atlona_AT-UHD-PRO3-88M_IP.c4i"
+     * Config-URL: "http://192.168.1.30/"
+     * 
+ */ + @Override + public @Nullable ThingUID getThingUID(SddpDevice device) { + if (device.manufacturer.toUpperCase(Locale.ENGLISH).contains(ATLONA) + && PROXY_AVSWITCH.equals(device.primaryProxy) && !device.macAddress.isBlank() + && !device.ipAddress.isBlank()) { + final ThingTypeUID typeId; + + switch (device.model) { + case "AT-UHD-PRO3-44M": + typeId = THING_TYPE_PRO3_44M; + break; + case "AT-UHD-PRO3-66M": + typeId = THING_TYPE_PRO3_66M; + break; + case "AT-UHD-PRO3-88M": + typeId = THING_TYPE_PRO3_88M; + break; + case "AT-UHD-PRO3-1616M": + typeId = THING_TYPE_PRO3_1616M; + break; + case "AT-PRO3HD44M": + typeId = THING_TYPE_PRO3HD_44M; + break; + case "AT-PRO3HD66M": + typeId = THING_TYPE_PRO3HD_66M; + break; + default: + logger.warn("Unknown model #: {}", device.model); + return null; + } + + logger.debug("Atlona matrix with mac {} found at {}", device.macAddress, device.ipAddress); + return new ThingUID(typeId, device.macAddress); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/addon/addon.xml index 491c7b8940f..ff8c4578874 100644 --- a/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.binding.atlona/src/main/resources/OH-INF/addon/addon.xml @@ -8,4 +8,20 @@ Binding for Atlona PRO3 HDBaseT Matrix switches. local + + + sddp + + + manufacturer + (?i).*atlona.* + + + model + (?i).*(AT-UHD-PRO3|AT-PRO3HD).* + + + + +