# 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)
## 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
*/
package org.openhab.binding.benqprojector.internal;
+import java.util.Set;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
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";
}
--- /dev/null
+/**
+ * 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");
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
}
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);
+ }
}
}
}
# 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