[benqprojector] Add discovery service (#12866)

* 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
6 changed files with 310 additions and 9 deletions

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

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

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

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

View 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