[benqprojector] Add discovery service ()

* Add discovery service

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
mlobstein 2022-06-04 16:47:32 -05:00 committed by GitHub
parent 3de409be75
commit 22d0e5905c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 310 additions and 9 deletions
bundles/org.openhab.binding.benqprojector

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

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

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

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

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

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