[benqprojector] Add discovery service (#12866)
* Add discovery service Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
parent
3de409be75
commit
22d0e5905c
bundles/org.openhab.binding.benqprojector
README.md
src/main
java/org/openhab/binding/benqprojector/internal
resources/OH-INF/i18n
@ -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";
|
||||
}
|
||||
|
155
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/BenqProjectorDiscoveryService.java
Normal file
155
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/BenqProjectorDiscoveryService.java
Normal file
@ -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");
|
||||
}
|
||||
}
|
133
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/MulticastListener.java
Normal file
133
bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/discovery/MulticastListener.java
Normal file
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user