[epsonprojector] Add ESC/VP.net handshake for projectors with built-in ethernet (#9375)
Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.epsonprojector.internal;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
@@ -20,18 +22,28 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Yannick Schaus - Initial contribution
|
||||
* @author Michael Lobstein - Updated for OH3
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EpsonProjectorBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "epsonprojector";
|
||||
public static final String BINDING_ID = "epsonprojector";
|
||||
public static final int DEFAULT_PORT = 3629;
|
||||
|
||||
// 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";
|
||||
public static final String CHANNEL_TYPE_POWERSTATE = "powerstate";
|
||||
public static final String CHANNEL_TYPE_LAMPTIME = "lamptime";
|
||||
|
||||
// Config properties
|
||||
public static final String THING_PROPERTY_HOST = "host";
|
||||
public static final String THING_PROPERTY_PORT = "port";
|
||||
public static final String THING_PROPERTY_MAC = "macAddress";
|
||||
}
|
||||
|
||||
@@ -137,12 +137,13 @@ public class EpsonProjectorDevice {
|
||||
return response;
|
||||
}
|
||||
|
||||
private String splitResponse(@Nullable String response) throws EpsonProjectorException {
|
||||
private String splitResponse(@Nullable String response)
|
||||
throws EpsonProjectorCommandException, EpsonProjectorException {
|
||||
if (response != null && !"".equals(response)) {
|
||||
String[] pieces = response.split("=");
|
||||
|
||||
if (pieces.length < 2) {
|
||||
throw new EpsonProjectorException("Invalid response from projector: " + response);
|
||||
throw new EpsonProjectorCommandException("Invalid response from projector: " + response);
|
||||
}
|
||||
|
||||
return pieces[1].trim();
|
||||
|
||||
@@ -14,11 +14,6 @@ package org.openhab.binding.epsonprojector.internal;
|
||||
|
||||
import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.epsonprojector.internal.handler.EpsonProjectorHandler;
|
||||
@@ -42,9 +37,6 @@ import org.osgi.service.component.annotations.Reference;
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.epsonprojector", service = ThingHandlerFactory.class)
|
||||
public class EpsonProjectorHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(THING_TYPE_PROJECTOR_SERIAL, THING_TYPE_PROJECTOR_TCP).collect(Collectors.toSet()));
|
||||
private final SerialPortManager serialPortManager;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.epsonprojector.internal.connector;
|
||||
|
||||
import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.DEFAULT_PORT;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -32,6 +34,7 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EpsonProjectorTcpConnector implements EpsonProjectorConnector {
|
||||
private static final String ESC_VP_HANDSHAKE = "ESC/VP.net\u0010\u0003\u0000\u0000\u0000\u0000";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(EpsonProjectorTcpConnector.class);
|
||||
private final String ip;
|
||||
@@ -58,6 +61,16 @@ public class EpsonProjectorTcpConnector implements EpsonProjectorConnector {
|
||||
} catch (IOException e) {
|
||||
throw new EpsonProjectorException(e);
|
||||
}
|
||||
|
||||
// Projectors with built in Ethernet listen on 3629, we must send the handshake to initialize the connection
|
||||
if (port == DEFAULT_PORT) {
|
||||
try {
|
||||
String response = sendMessage(ESC_VP_HANDSHAKE, 5000);
|
||||
logger.debug("Response to initialisation of ESC/VP.net is: {}", response);
|
||||
} catch (EpsonProjectorException e) {
|
||||
logger.debug("Error within initialisation of ESC/VP.net: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.epsonprojector.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.HashMap;
|
||||
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.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
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 EpsonProjectoreDiscoveryService} class implements a service
|
||||
* for discovering Epson projectors using the AMX Device Discovery protocol.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
* @author Michael Lobstein - Adapted for the Epson Projector binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, configurationPid = "discovery.epsonprojector")
|
||||
public class EpsonProjectorDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(EpsonProjectorDiscoveryService.class);
|
||||
private @Nullable ScheduledFuture<?> epsonDiscoveryJob;
|
||||
|
||||
// Discovery parameters
|
||||
public static final boolean BACKGROUND_DISCOVERY_ENABLED = true;
|
||||
public static final int BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC = 10;
|
||||
|
||||
private NetworkAddressService networkAddressService;
|
||||
|
||||
private boolean terminate = false;
|
||||
|
||||
@Activate
|
||||
public EpsonProjectorDiscoveryService(@Reference NetworkAddressService networkAddressService) {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, 0, BACKGROUND_DISCOVERY_ENABLED);
|
||||
this.networkAddressService = networkAddressService;
|
||||
epsonDiscoveryJob = null;
|
||||
terminate = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||
return SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
if (epsonDiscoveryJob == null) {
|
||||
terminate = false;
|
||||
logger.debug("Starting background discovery job in {} seconds", BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC);
|
||||
epsonDiscoveryJob = scheduler.schedule(this::discover, BACKGROUND_DISCOVERY_DELAY_TIMEOUT_SEC,
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
ScheduledFuture<?> epsonDiscoveryJob = this.epsonDiscoveryJob;
|
||||
if (epsonDiscoveryJob != null) {
|
||||
terminate = true;
|
||||
epsonDiscoveryJob.cancel(false);
|
||||
this.epsonDiscoveryJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopScan() {
|
||||
}
|
||||
|
||||
private synchronized void discover() {
|
||||
logger.debug("Discovery job is running");
|
||||
MulticastListener epsonMulticastListener;
|
||||
String local = "127.0.0.1";
|
||||
|
||||
try {
|
||||
String ip = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
epsonMulticastListener = new MulticastListener((ip != null ? ip : local));
|
||||
} catch (SocketException se) {
|
||||
logger.debug("Discovery job got Socket exception creating multicast socket: {}", se.getMessage());
|
||||
return;
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Discovery job got IO exception creating multicast socket: {}", ioe.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
while (!terminate) {
|
||||
boolean beaconReceived;
|
||||
try {
|
||||
// Wait for a discovery beacon.
|
||||
beaconReceived = epsonMulticastListener.waitForBeacon();
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Discovery job got exception waiting for beacon: {}", ioe.getMessage());
|
||||
beaconReceived = false;
|
||||
}
|
||||
|
||||
if (beaconReceived) {
|
||||
// We got a discovery beacon. Process it as a potential new thing
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
String uid = epsonMulticastListener.getUID();
|
||||
|
||||
properties.put(THING_PROPERTY_HOST, epsonMulticastListener.getIPAddress());
|
||||
properties.put(THING_PROPERTY_PORT, DEFAULT_PORT);
|
||||
|
||||
logger.trace("Projector with UID {} discovered at IP: {}", uid, epsonMulticastListener.getIPAddress());
|
||||
|
||||
ThingUID thingUid = new ThingUID(THING_TYPE_PROJECTOR_TCP, uid);
|
||||
logger.trace("Creating epson projector discovery result for: {}, IP={}", uid,
|
||||
epsonMulticastListener.getIPAddress());
|
||||
thingDiscovered(DiscoveryResultBuilder.create(thingUid).withProperties(properties)
|
||||
.withLabel("Epson Projector " + uid).withProperty(THING_PROPERTY_MAC, uid)
|
||||
.withRepresentationProperty(THING_PROPERTY_MAC).build());
|
||||
}
|
||||
}
|
||||
epsonMulticastListener.shutdown();
|
||||
logger.debug("Discovery job is exiting");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.epsonprojector.internal.discovery;
|
||||
|
||||
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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link MulticastListener} class is responsible for listening for the Epson 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 Epson Projector binding
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MulticastListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(MulticastListener.class);
|
||||
|
||||
private MulticastSocket socket;
|
||||
|
||||
// Epson-specific properties defined in this binding
|
||||
private String uid = "";
|
||||
private String ipAddress = "";
|
||||
|
||||
// Epson projector devices announce themselves on a multicast port
|
||||
private static final String EPSON_MULTICAST_GROUP = "239.255.250.250";
|
||||
private static final int EPSON_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(EPSON_MULTICAST_PORT);
|
||||
socket.setInterface(ifAddress);
|
||||
socket.setSoTimeout(DEFAULT_SOCKET_TIMEOUT_SEC);
|
||||
InetAddress mcastAddress = InetAddress.getByName(EPSON_MULTICAST_GROUP);
|
||||
socket.joinGroup(mcastAddress);
|
||||
logger.debug("Multicast listener joined multicast group {}:{}", EPSON_MULTICAST_GROUP, EPSON_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 false on socket timeout or error.
|
||||
* Otherwise, parse the beacon for information about the device.
|
||||
*/
|
||||
public boolean 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) {
|
||||
// Get the device properties from the announcement beacon
|
||||
parseAnnouncementBeacon(msgPacket);
|
||||
}
|
||||
|
||||
return beaconFound;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse the announcement beacon into the elements needed to create the thing.
|
||||
*
|
||||
* Example Epson beacon:
|
||||
* AMXB<-UUID=000048746B33><-SDKClass=VideoProjector><-GUID=EPSON_EMP001><-Revision=1.0.0>
|
||||
*/
|
||||
private void parseAnnouncementBeacon(DatagramPacket packet) {
|
||||
String beacon = (new String(packet.getData(), StandardCharsets.UTF_8)).trim();
|
||||
|
||||
logger.trace("Multicast listener parsing announcement packet: {}", beacon);
|
||||
|
||||
clearProperties();
|
||||
|
||||
if (beacon.toUpperCase().contains("EPSON") && beacon.toUpperCase().contains("VIDEOPROJECTOR")) {
|
||||
ipAddress = packet.getAddress().getHostAddress();
|
||||
parseEpsonAnnouncementBeacon(beacon);
|
||||
} else {
|
||||
logger.debug("Multicast listener doesn't know how to parse beacon: {}", beacon);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseEpsonAnnouncementBeacon(String beacon) {
|
||||
String[] parameterList = beacon.split("<-");
|
||||
|
||||
for (String parameter : parameterList) {
|
||||
String[] keyValue = parameter.split("=");
|
||||
|
||||
if (keyValue.length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyValue[0].contains("UUID")) {
|
||||
uid = keyValue[1].substring(0, keyValue[1].length() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearProperties() {
|
||||
uid = "";
|
||||
ipAddress = "";
|
||||
}
|
||||
|
||||
public String getUID() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public String getIPAddress() {
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@
|
||||
|
||||
<thing-type id="projector-tcp">
|
||||
<label>Epson Projector - TCP/IP</label>
|
||||
<description>An Epson projector which supports the ESC/VP21 protocol via a serial over IP connection</description>
|
||||
<description>An Epson projector which supports the ESC/VP21 protocol via the built-in ethernet port or a serial over
|
||||
IP connection</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="power"/>
|
||||
@@ -89,15 +90,18 @@
|
||||
<channel id="errmessage" typeId="errmessage"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>macAddress</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Host</label>
|
||||
<context>network-address</context>
|
||||
<description>IP address for the serial over IP device</description>
|
||||
<description>IP address for the projector or serial over IP device</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" min="1" max="65535" required="true">
|
||||
<label>Port</label>
|
||||
<description>Port for the serial over IP device</description>
|
||||
<description>Port for the projector or serial over IP device</description>
|
||||
<default>3629</default>
|
||||
</parameter>
|
||||
<parameter name="pollingInterval" type="integer" min="5" max="60" unit="s" required="false">
|
||||
<label>Polling interval</label>
|
||||
|
||||
Reference in New Issue
Block a user