diff --git a/bundles/org.openhab.binding.benqprojector/README.md b/bundles/org.openhab.binding.benqprojector/README.md index 815b88fbf..aae33af4d 100644 --- a/bundles/org.openhab.binding.benqprojector/README.md +++ b/bundles/org.openhab.binding.benqprojector/README.md @@ -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 diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java index 1114420d1..faba31b36 100644 --- a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java @@ -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 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 index 000000000..536995459 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/BenqProjectorDiscoveryService.java @@ -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 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 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 index 000000000..3cfef02bd --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/MulticastListener.java @@ -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 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 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 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; + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java index 9f42639ec..d212e78fa 100644 --- a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java @@ -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); + } } } } diff --git a/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/i18n/benqprojector.properties b/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/i18n/benqprojector.properties index 20f95a3e9..37b6fbbc1 100644 --- a/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/i18n/benqprojector.properties +++ b/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/i18n/benqprojector.properties @@ -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