]> git.basschouten.com Git - openhab-addons.git/commitdiff
[epsonprojector] Add ESC/VP.net handshake for projectors with built-in ethernet ...
authormlobstein <michael.lobstein@gmail.com>
Fri, 25 Dec 2020 14:44:59 +0000 (08:44 -0600)
committerGitHub <noreply@github.com>
Fri, 25 Dec 2020 14:44:59 +0000 (15:44 +0100)
Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
bundles/org.openhab.binding.epsonprojector/README.md
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorBindingConstants.java
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorDevice.java
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorHandlerFactory.java
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorTcpConnector.java
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/discovery/EpsonProjectorDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/discovery/MulticastListener.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml

index e2b31391ea819f373898703792415608ce0885f3..9be2d439fe5c86151ad48f7ea7089f692f34106e 100644 (file)
@@ -1,7 +1,7 @@
 # Epson Projector Binding
 
-This binding is compatible with Epson projectors that support the ESC/VP21 protocol over a serial port or USB to serial adapter.
-Alternatively, you can connect to your projector via a TCP connection using a serial over IP device or by using`ser2net`.
+This binding is compatible with Epson projectors that support the ESC/VP21 protocol over the built-in ethernet port, serial port or USB to serial adapter.
+If your projector does not have a built-in ethernet port, you can connect to your projector's serial port via a TCP connection using a serial over IP device or by using`ser2net`.
 
 ## Supported Things
 
@@ -9,7 +9,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 the 'AMX Device Discovery' option is present and enabled in the projector's network menu, the thing will be discovered automatically.
+Serial port or serial over IP connections must be configured manually.
 
 ## Binding Configuration
 
@@ -25,8 +26,8 @@ The `projector-serial` thing has the following configuration parameters:
 
 The `projector-tcp` thing has the following configuration parameters:
 
-- _host_: IP address for the serial over IP device
-- _port_: Port for the serial over IP device
+- _host_: IP address for the projector or serial over IP device
+- _port_: Port for the projector or serial over IP device; default 3629 for projectors with built-in ethernet connector
 - _pollingInterval_: Polling interval in seconds to update channel states | 5-60 seconds; default 10 seconds
 
 Some notes:
@@ -38,6 +39,7 @@ Some notes:
 * The following channels _aspectratio_, _colormode_, _luminance_, _gamma_ and _background_ are pre-populated with a full set of options and not every option will be useable on all projectors.
 * If your projector has an option in one of the above mentioned channels that is not recognized by the binding, the channel will display 'UNKNOWN' if that un-recognized option is selected by the remote control.
 * If the projector power is switched to off in the middle of a polling operation, some of the channel values may become undefined until the projector is switched on again.
+* If the binding fails to connect to the projector using the direct IP connection, ensure that no password is configured on the projctor.
 
 * On Linux, you may get an error stating the serial port cannot be opened when the epsonprojector binding tries to load.
 * You can get around this by adding the `openhab` user to the `dialout` group like this: `usermod -a -G dialout openhab`.
@@ -86,11 +88,11 @@ Some notes:
 things/epson.things:
 
 ```java
-//serial port connection
+// serial port connection
 epsonprojector:projector-serial:hometheater "Projector" [ serialPort="COM5", pollingInterval=10 ]
 
-// serial over IP connection
-epsonprojector:projector-tcp:hometheater "Projector"  [ host="192.168.0.10", port=4444, pollingInterval=10 ]
+// direct IP or serial over IP connection
+epsonprojector:projector-tcp:hometheater "Projector"  [ host="192.168.0.10", port=3629, pollingInterval=10 ]
 
 ```
 
index 28439cec508d000e2b7d70aab3937d155e03e466..7a7c656d4b6e42803204da14e30280cae0f56424 100644 (file)
@@ -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";
 }
index 37875759ab366e1f7079774d9cc596d25b5fbc9f..a2afe2382e6faefe42a94b4d53b2209bbf141a59 100644 (file)
@@ -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();
index 815e06701d23ca90e27dea0bc33604fcd4978ad8..fd3e6aea9f8c0c3d39b3791d57e9f65c53b8a363 100644 (file)
@@ -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
index aea7eefc4951bd455f44860b513b3331212f4b52..633660f40bfac4c934b1ae06bb9efdf6b5d07f76 100644 (file)
@@ -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
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/discovery/EpsonProjectorDiscoveryService.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/discovery/EpsonProjectorDiscoveryService.java
new file mode 100644 (file)
index 0000000..eae20ff
--- /dev/null
@@ -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");
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/discovery/MulticastListener.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/discovery/MulticastListener.java
new file mode 100644 (file)
index 0000000..d03f4ee
--- /dev/null
@@ -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;
+    }
+}
index e4302be7b12c14beff43a260a0b1d8c19af026eb..cd2969343d2474840b3e4d5982f8282c6e2c1069 100644 (file)
@@ -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"/>
                        <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>