]> git.basschouten.com Git - openhab-addons.git/commitdiff
[benqprojector] Add discovery service (#12866)
authormlobstein <michael.lobstein@gmail.com>
Sat, 4 Jun 2022 21:47:32 +0000 (16:47 -0500)
committerGitHub <noreply@github.com>
Sat, 4 Jun 2022 21:47:32 +0000 (23:47 +0200)
* Add discovery service

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
bundles/org.openhab.binding.benqprojector/README.md
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/BenqProjectorDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/MulticastListener.java [new file with mode: 0644]
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java
bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/i18n/benqprojector.properties

index 815b88fbf83321e45dc007789f650f65497c59f3..aae33af4d170631ea3a4b500eaa6937573cd849a 100644 (file)
@@ -1,6 +1,6 @@
 # BenQ Projector Binding
 
-This binding is compatible with BenQ projectors that support the control protocol via the built-in ethernet port, serial port or USB to serial adapter.
+This binding is compatible with BenQ projectors that support the control protocol via the built-in Ethernet port, serial port or USB to serial adapter.
 If your projector does not have built-in networking, you can connect to your projector's serial port via a TCP connection using a serial over IP device or by using`ser2net`.  
 
 The manufacturer's guide for connecting to the projector and the control protocol can be found in this document: [LX9215_RS232 Control Guide_0_Windows7_Windows8_WinXP.pdf](https://esupportdownload.benq.com/esupport/Projector/Control%20Protocols/LX9215/LX9215_RS232%20Control%20Guide_0_Windows7_Windows8_WinXP.pdf)
@@ -11,7 +11,8 @@ This binding supports two thing types based on the connection used: `projector-s
 
 ## Discovery
 
-The projector thing cannot be auto-discovered, it has to be configured manually.
+If the projector has a built-in Ethernet port connected to the same network as the openHAB server and supports AMX Device Discovery, the thing will be discovered automatically.
+Serial port or serial over IP connections must be configured manually.
 
 ## Binding Configuration
 
index 1114420d1bc286fb4f6a228551756f9a4a8092c2..faba31b36bda9d7f7e8ddf845e0b15b54fe76f49 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.benqprojector.internal;
 
+import java.util.Set;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.thing.ThingTypeUID;
 
@@ -25,11 +27,19 @@ import org.openhab.core.thing.ThingTypeUID;
 public class BenqProjectorBindingConstants {
 
     private static final String BINDING_ID = "benqprojector";
+    public static final int DEFAULT_PORT = 8000;
 
     // List of all Thing Type UIDs
     public static final ThingTypeUID THING_TYPE_PROJECTOR_SERIAL = new ThingTypeUID(BINDING_ID, "projector-serial");
     public static final ThingTypeUID THING_TYPE_PROJECTOR_TCP = new ThingTypeUID(BINDING_ID, "projector-tcp");
 
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PROJECTOR_SERIAL,
+            THING_TYPE_PROJECTOR_TCP);
+
     // Some Channel types
     public static final String CHANNEL_TYPE_POWER = "power";
+
+    // Config properties
+    public static final String THING_PROPERTY_HOST = "host";
+    public static final String THING_PROPERTY_PORT = "port";
 }
diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/BenqProjectorDiscoveryService.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/BenqProjectorDiscoveryService.java
new file mode 100644 (file)
index 0000000..5369954
--- /dev/null
@@ -0,0 +1,155 @@
+/**
+ * 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.benqprojector.internal.discovery;
+
+import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.*;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link BenqProjectorDiscoveryService} class implements a service
+ * for discovering BenQ projectors using the AMX Device Discovery protocol.
+ *
+ * @author Mark Hilbush - Initial contribution
+ * @author Michael Lobstein - Adapted for the BenQ Projector binding
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, configurationPid = "discovery.benqprojector")
+public class BenqProjectorDiscoveryService extends AbstractDiscoveryService {
+    private final Logger logger = LoggerFactory.getLogger(BenqProjectorDiscoveryService.class);
+    private @Nullable ScheduledFuture<?> benqDiscoveryJob;
+
+    // Discovery parameters
+    public static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
+    public static final int BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC = 10;
+
+    private NetworkAddressService networkAddressService;
+    private final TranslationProvider translationProvider;
+    private final LocaleProvider localeProvider;
+    private final @Nullable Bundle bundle;
+
+    private boolean terminate = false;
+
+    @Activate
+    public BenqProjectorDiscoveryService(@Reference NetworkAddressService networkAddressService,
+            @Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider) {
+        super(SUPPORTED_THING_TYPES_UIDS, 0, BACKGROUND_DISCOVERY_ENABLED);
+        this.networkAddressService = networkAddressService;
+        this.translationProvider = translationProvider;
+        this.localeProvider = localeProvider;
+        this.bundle = FrameworkUtil.getBundle(BenqProjectorDiscoveryService.class);
+
+        benqDiscoveryJob = null;
+        terminate = false;
+    }
+
+    @Override
+    public Set<ThingTypeUID> getSupportedThingTypes() {
+        return SUPPORTED_THING_TYPES_UIDS;
+    }
+
+    @Override
+    protected void startBackgroundDiscovery() {
+        if (benqDiscoveryJob == null) {
+            terminate = false;
+            logger.debug("Starting background discovery job in {} seconds", BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC);
+            benqDiscoveryJob = scheduler.schedule(this::discover, BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC,
+                    TimeUnit.SECONDS);
+        }
+    }
+
+    @Override
+    protected void stopBackgroundDiscovery() {
+        ScheduledFuture<?> benqDiscoveryJob = this.benqDiscoveryJob;
+        if (benqDiscoveryJob != null) {
+            terminate = true;
+            benqDiscoveryJob.cancel(false);
+            this.benqDiscoveryJob = null;
+        }
+    }
+
+    @Override
+    public void startScan() {
+    }
+
+    @Override
+    public void stopScan() {
+    }
+
+    private synchronized void discover() {
+        logger.debug("Discovery job is running");
+        MulticastListener benqMulticastListener;
+        String local = "127.0.0.1";
+
+        try {
+            String ip = networkAddressService.getPrimaryIpv4HostAddress();
+            benqMulticastListener = new MulticastListener((ip != null ? ip : local));
+        } catch (IOException ioe) {
+            logger.debug("Discovery job got IO exception creating multicast socket: {}", ioe.getMessage());
+            return;
+        }
+
+        while (!terminate) {
+            try {
+                // Wait for a discovery beacon to return properties for a BenQ projector.
+                Map<String, Object> thingProperties = benqMulticastListener.waitForBeacon();
+
+                if (thingProperties != null) {
+                    // The MulticastListener found a projector, add it as new thing
+                    String uid = (String) thingProperties.get(Thing.PROPERTY_MAC_ADDRESS);
+                    String ipAddress = (String) thingProperties.get(THING_PROPERTY_HOST);
+
+                    if (uid != null) {
+                        logger.trace("Projector with UID {} discovered at IP: {}", uid, ipAddress);
+
+                        ThingUID thingUid = new ThingUID(THING_TYPE_PROJECTOR_TCP, uid);
+                        logger.trace("Creating BenQ projector discovery result for: {}, IP={}", uid, ipAddress);
+                        thingDiscovered(
+                                DiscoveryResultBuilder.create(thingUid).withProperties(thingProperties)
+                                        .withLabel(translationProvider.getText(bundle,
+                                                "thing-type.benqprojector.discovery.label", "BenQ Projector",
+                                                localeProvider.getLocale()) + " " + uid)
+                                        .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build());
+                    }
+                }
+            } catch (IOException ioe) {
+                logger.debug("Discovery job got exception waiting for beacon: {}", ioe.getMessage());
+            }
+        }
+        benqMulticastListener.shutdown();
+        logger.debug("Discovery job is exiting");
+    }
+}
diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/MulticastListener.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/MulticastListener.java
new file mode 100644 (file)
index 0000000..3cfef02
--- /dev/null
@@ -0,0 +1,133 @@
+/**
+ * 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.benqprojector.internal.discovery;
+
+import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.DEFAULT_PORT;
+import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.THING_PROPERTY_HOST;
+import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.THING_PROPERTY_PORT;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MulticastListener} class is responsible for listening for the BenQ projector device announcement
+ * beacons on the multicast address, and then extracting the data fields out of the received datagram.
+ *
+ * @author Mark Hilbush - Initial contribution
+ * @author Michael Lobstein - Adapted for the BenQ Projector binding
+ */
+@NonNullByDefault
+public class MulticastListener {
+    private final Logger logger = LoggerFactory.getLogger(MulticastListener.class);
+
+    private MulticastSocket socket;
+
+    // BenQ projector devices announce themselves on the AMX DDD multicast port
+    private static final String AMX_MULTICAST_GROUP = "239.255.250.250";
+    private static final int AMX_MULTICAST_PORT = 9131;
+
+    // How long to wait in milliseconds for a discovery beacon
+    public static final int DEFAULT_SOCKET_TIMEOUT_SEC = 3000;
+
+    /*
+     * Constructor joins the multicast group, throws IOException on failure.
+     */
+    public MulticastListener(String ipv4Address) throws IOException, SocketException {
+        InetAddress ifAddress = InetAddress.getByName(ipv4Address);
+        logger.debug("Discovery job using address {} on network interface {}", ifAddress.getHostAddress(),
+                NetworkInterface.getByInetAddress(ifAddress).getName());
+        socket = new MulticastSocket(AMX_MULTICAST_PORT);
+        socket.setInterface(ifAddress);
+        socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_SEC);
+        InetAddress mcastAddress = InetAddress.getByName(AMX_MULTICAST_GROUP);
+        socket.joinGroup(mcastAddress);
+        logger.debug("Multicast listener joined multicast group {}:{}", AMX_MULTICAST_GROUP, AMX_MULTICAST_PORT);
+    }
+
+    public void shutdown() {
+        logger.debug("Multicast listener closing down multicast socket");
+        socket.close();
+    }
+
+    /*
+     * Wait on the multicast socket for an announcement beacon. Return null on socket timeout or error.
+     * Otherwise, parse the beacon for information about the device and return the device properties.
+     */
+    public @Nullable Map<String, Object> waitForBeacon() throws IOException {
+        byte[] bytes = new byte[600];
+        boolean beaconFound;
+
+        // Wait for a device to announce itself
+        logger.trace("Multicast listener waiting for datagram on multicast port");
+        DatagramPacket msgPacket = new DatagramPacket(bytes, bytes.length);
+        try {
+            socket.receive(msgPacket);
+            beaconFound = true;
+            logger.trace("Multicast listener got datagram of length {} from multicast port: {}", msgPacket.getLength(),
+                    msgPacket.toString());
+        } catch (SocketTimeoutException e) {
+            beaconFound = false;
+        }
+
+        if (beaconFound) {
+            // Return the device properties from the announcement beacon
+            return parseAnnouncementBeacon(msgPacket);
+        }
+
+        return null;
+    }
+
+    /*
+     * Parse the announcement beacon into the elements needed to create the thing.
+     *
+     * Example beacon:
+     * AMXB<-UUID=000048746B33><-SDKClass=VideoProjector><-GUID=EPSON_EMP001><-Revision=1.0.0>
+     */
+    private @Nullable Map<String, Object> parseAnnouncementBeacon(DatagramPacket packet) {
+        String beacon = (new String(packet.getData(), StandardCharsets.UTF_8)).trim();
+        logger.trace("Multicast listener parsing announcement packet: {}", beacon);
+
+        if (beacon.toUpperCase(Locale.ENGLISH).contains("BENQ") && beacon.contains("VideoProjector")) {
+            String[] parameterList = beacon.replace(">", "").split("<-");
+
+            for (String parameter : parameterList) {
+                String[] keyValue = parameter.split("=");
+
+                if (keyValue.length == 2 && keyValue[0].contains("UUID") && !keyValue[1].isEmpty()) {
+                    Map<String, Object> properties = new HashMap<>();
+                    properties.put(Thing.PROPERTY_MAC_ADDRESS, keyValue[1]);
+                    properties.put(THING_PROPERTY_HOST, packet.getAddress().getHostAddress());
+                    properties.put(THING_PROPERTY_PORT, DEFAULT_PORT);
+                    return properties;
+                }
+            }
+            logger.debug("Multicast listener doesn't know how to parse beacon: {}", beacon);
+        }
+        return null;
+    }
+}
index 9f42639ec44c3be578addaede4f14e4ed4ede018..d212e78fa068ed8702630ff1fdb6de6da1b4cf5f 100644 (file)
@@ -279,13 +279,14 @@ public class BenqProjectorHandler extends BaseThingHandler {
     }
 
     private void closeConnection() {
-        BenqProjectorDevice remoteController = device.get();
-        try {
-            logger.debug("Closing connection to device '{}'", this.thing.getUID());
-            remoteController.disconnect();
-            updateStatus(ThingStatus.OFFLINE);
-        } catch (BenqProjectorException e) {
-            logger.debug("Error occurred when closing connection to device '{}'", this.thing.getUID(), e);
+        if (device.isPresent()) {
+            try {
+                logger.debug("Closing connection to device '{}'", this.thing.getUID());
+                device.get().disconnect();
+                updateStatus(ThingStatus.OFFLINE);
+            } catch (BenqProjectorException e) {
+                logger.debug("Error occurred when closing connection to device '{}'", this.thing.getUID(), e);
+            }
         }
     }
 }
index 20f95a3e99170595dbc813dc071ef6eff749e8c8..37b6fbbc1f955b704cc34dfe877811be2852e600 100644 (file)
@@ -5,6 +5,7 @@ binding.benqprojector.description = This binding is compatible with BenQ project
 
 # thing types
 
+thing-type.benqprojector.discovery.label = BenQ Projector
 thing-type.benqprojector.projector-serial.label = BenQ Projector - Serial
 thing-type.benqprojector.projector-serial.description = A BenQ projector connected via a serial port
 thing-type.benqprojector.projector-tcp.label = BenQ Projector - TCP/IP