]> git.basschouten.com Git - openhab-addons.git/commitdiff
[epsonprojector] Update epsonprojector binding for OH3 (#9021)
authormlobstein <michael.lobstein@gmail.com>
Fri, 4 Dec 2020 01:34:23 +0000 (19:34 -0600)
committerGitHub <noreply@github.com>
Fri, 4 Dec 2020 01:34:23 +0000 (17:34 -0800)
* baseline EpsonProjector code from ysc
* Improvements for OH3
* Finish epsonprojector binding for OH3
* improve exception logging
* cleanup exception logging
* Make connection specific thing types

Signed-off-by: Michael Lobstein <michael.lobstein@gmail.com>
28 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.epsonprojector/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/README.md [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandException.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandType.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorException.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/configuration/EpsonProjectorConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorSerialConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorTcpConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/AspectRatio.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Background.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ColorMode.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ErrorMessage.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Gamma.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Luminance.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/PowerStatus.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Switch.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index c6329d68d026bc5ac51cf38f9ff700d417aae628..c9b9feb2b26324ce11574e96198c2797d90c5ed2 100644 (file)
@@ -63,6 +63,7 @@
 /bundles/org.openhab.binding.enigma2/ @gdolfen
 /bundles/org.openhab.binding.enocean/ @fruggy83
 /bundles/org.openhab.binding.enturno/ @klocsson
+/bundles/org.openhab.binding.epsonprojector/ @mlobstein
 /bundles/org.openhab.binding.etherrain/ @dfad1469
 /bundles/org.openhab.binding.evohome/ @Nebula83
 /bundles/org.openhab.binding.exec/ @kgoderis
index 5f87670cbfa05decd3bbd460f082c7d114358442..450ff96f581229e863dae613024a30b7aeb38f46 100644 (file)
       <artifactId>org.openhab.binding.enturno</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.epsonprojector</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.etherrain</artifactId>
diff --git a/bundles/org.openhab.binding.epsonprojector/NOTICE b/bundles/org.openhab.binding.epsonprojector/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.epsonprojector/README.md b/bundles/org.openhab.binding.epsonprojector/README.md
new file mode 100644 (file)
index 0000000..e2b3139
--- /dev/null
@@ -0,0 +1,174 @@
+# 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`.
+
+## Supported Things
+
+This binding supports two thing types based on the connection used: `projector-serial` and `projector-tcp`.
+
+## Discovery
+
+The projector thing cannot be auto-discovered, it has to be configured manually.
+
+## Binding Configuration
+
+There are no overall binding configuration settings that need to be set.
+All settings are through thing configuration parameters.
+
+## Thing Configuration
+
+The `projector-serial` thing has the following configuration parameters:
+
+- _serialPort_: Serial port device name that is connected to the Epson projector to control, e.g. COM1 on Windows, /dev/ttyS0 on Linux or /dev/tty.PL2303-0000103D on Mac
+- _pollingInterval_: Polling interval in seconds to update channel states | 5-60 seconds; default 10 seconds
+
+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
+- _pollingInterval_: Polling interval in seconds to update channel states | 5-60 seconds; default 10 seconds
+
+Some notes:
+
+* The binding should work on all Epson projectors that support the ESC/VP21 protocol, however not all binding channels will be useable on all projectors.
+* The _source_ channel includes a dropdown with the most common source inputs.
+* If your projector has a source input that is not in the dropdown, the two digit hex code to access that input will be displayed by the _source_ channel when that input is selected by the remote control.
+* By using the sitemap mapping or a rule to send the input's code back to the _source_ channel, any source on the projector can be accessed by the binding.
+* 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.
+
+* 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`.
+* Also on Linux you may have issues with the USB if using two serial USB devices e.g. epsonprojector and RFXcom. See the [general documentation about serial port configuration](/docs/administration/serial.html) for more on symlinking the USB ports.
+* Here is an example of ser2net.conf you can use to share your serial port /dev/ttyUSB0 on IP port 4444 using [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) (take care, the baud rate is specific to the Epson projector):
+
+```
+4444:raw:0:/dev/ttyUSB0:9600 8DATABITS NONE 1STOPBIT LOCAL
+```
+
+## Channels
+
+| Channel            | Item Type | Purpose                                             | Values    | 
+| ------------------ | --------- | --------------------------------------------------- | --------- | 
+| power              | Switch    | Powers the projector on or off.                     |           | 
+| powerstate         | String    | Retrieves the textual power state of the projector. | Read only | 
+| source             | String    | Retrieve or set the input source.                   | See above | 
+| aspectratio        | String    | Retrieve or set the aspect ratio.                   | See above | 
+| colormode          | String    | Retrieve or set the color mode.                     | See above | 
+| freeze             | Switch    | Turn the freeze screen mode on or off.              |           | 
+| mute               | Switch    | Turn the AV mute on or off.                         |           | 
+| volume             | Number    | Retrieve or set the volume.                         | 0   - +20 | 
+| luminance          | String    | Retrieve or set the lamp mode.                      | See above | 
+| brightness         | Number    | Retrieve or set the brightness.                     | -24 - +24 | 
+| contrast           | Number    | Retrieve or set the contrast.                       | -24 - +24 | 
+| density            | Number    | Retrieve or set the density (color saturation).     | -32 - +32 | 
+| tint               | Number    | Retrieve or set the tint.                           | -32 - +32 | 
+| colortemperature   | Number    | Retrieve or set the color temperature.              | 0   - +9  | 
+| fleshtemperature   | Number    | Retrieve or set the flesh temperature.              | 0   - +6  | 
+| gamma              | String    | Retrieve or set the gamma setting.                  | See above | 
+| autokeystone       | Switch    | Turn the auto keystone mode on or off.              |           | 
+| verticalkeystone   | Number    | Retrieve or set the vertical keystone.              | -30 - +30 | 
+| horizontalkeystone | Number    | Retrieve or set the horizontal keystone.            | -30 - +30 | 
+| verticalposition   | Number    | Retrieve or set the vertical position.              | -8  - +10 | 
+| horizontalposition | Number    | Retrieve or set the horizontal position.            | -23 - +26 | 
+| verticalreverse    | Switch    | Turn the vertical reverse mode on or off.           |           | 
+| horizontalreverse  | Switch    | Turn the horizontal reverse mode on or off.         |           | 
+| background         | String    | Retrieve or set the background color/logo.          | See above | 
+| keycode            | Number    | Send a key operation command to the projector.      | Send only | 
+| lamptime           | Number    | Retrieves the lamp hours.                           | Read only | 
+| errcode            | Number    | Retrieves the last error code.                      | Read only | 
+| errmessage         | String    | Retrieves the description of the last error.        | Read only | 
+
+## Full Example
+
+things/epson.things:
+
+```java
+//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 ]
+
+```
+
+items/epson.items
+
+```
+Switch epsonPower                                      { channel="epsonprojector:projector:hometheater:power" }
+String epsonSource       "Source [%s]"                 { channel="epsonprojector:projector:hometheater:source" }
+String epsonAspectRatio  "Aspect Ratio [%s]"           { channel="epsonprojector:projector:hometheater:aspectratio" }
+String epsonColorMode    "Color Mode [%s]"             { channel="epsonprojector:projector:hometheater:colormode" }
+Switch epsonFreeze                                     { channel="epsonprojector:projector:hometheater:freeze" }
+Switch epsonMute                                       { channel="epsonprojector:projector:hometheater:mute" }
+Number epsonVolume                                     { channel="epsonprojector:projector:hometheater:volume" }
+String epsonLuminance    "Lamp Mode [%s]"              { channel="epsonprojector:projector:hometheater:luminance" }
+
+Number epsonBrightness                                 { channel="epsonprojector:projector:hometheater:brightness" }
+Number epsonContrast                                   { channel="epsonprojector:projector:hometheater:contrast" }
+Number epsonDensity                                    { channel="epsonprojector:projector:hometheater:density" }
+Number epsonTint                                       { channel="epsonprojector:projector:hometheater:tint" }
+Number epsonColorTemperature                           { channel="epsonprojector:projector:hometheater:colortemperature" }
+Number epsonFleshTemperature                           { channel="epsonprojector:projector:hometheater:fleshtemperature" }
+String epsonGamma        "Gamma [%s]"                  { channel="epsonprojector:projector:hometheater:gamma" }
+
+Switch epsonAutokeystone                               { channel="epsonprojector:projector:hometheater:autokeystone" }
+Number epsonVerticalKeystone                           { channel="epsonprojector:projector:hometheater:verticalkeystone" }
+Number epsonHorizontalKeystone                         { channel="epsonprojector:projector:hometheater:horizontalkeystone" }
+Number epsonVerticalPosition                           { channel="epsonprojector:projector:hometheater:verticalposition" }
+Number epsonHorizontalPosition                         { channel="epsonprojector:projector:hometheater:horizontalposition" }
+Switch epsonVerticalReverse                            { channel="epsonprojector:projector:hometheater:verticalreverse" }
+Switch epsonHorizontalReverse                          { channel="epsonprojector:projector:hometheater:horizontalreverse" }
+
+String epsonBackground  "Background [%s]"              { channel="epsonprojector:projector:hometheater:background" }
+String epsonPowerState  "Power State [%s]"   <switch>  { channel="epsonprojector:projector:hometheater:powerstate" }
+Number epsonLampTime    "Lamp Time [%d h]"   <switch>       { channel="epsonprojector:projector:hometheater:lamptime" }
+Number epsonErrCode     "Error Code [%d]"    <"siren-on">   { channel="epsonprojector:projector:hometheater:errcode" }
+String epsonErrMessage  "Error Message [%s]" <"siren-off">  { channel="epsonprojector:projector:hometheater:errmessage" }
+```
+
+sitemaps/epson.sitemap
+
+```
+sitemap epson label="Epson Projector Demo"
+{
+    Frame label="Controls" {
+        Switch     item=epsonPower  label="Power"
+        Selection  item=epsonSource label="Source" mappings=["30"="HDMI1", "A0"="HDMI2", "14"="Component", "20"="PC DSUB", "41"="Video", "42"="S-Video"]
+        Switch     item=epsonFreeze label="Freeze"
+        Switch     item=epsonMute   label="AV Mute"
+        Setpoint   item=epsonVolume label="Volume"
+
+    }
+    Frame label="Adjust Image" {
+        Setpoint   item=epsonBrightness         label="Brightness"
+        Setpoint   item=epsonContrast           label="Contrast"
+        Setpoint   item=epsonDensity            label="Color Saturation"
+        Setpoint   item=epsonTint               label="Tint"
+        Switch     item=epsonAutokeystone       label="Auto Keystone"
+        Setpoint   item=epsonVerticalKeystone   label="Vertical Keystone"
+        Setpoint   item=epsonHorizontalKeystone label="Horizontal Keystone"
+        Setpoint   item=epsonVerticalPosition   label="Vertical Position"
+        Setpoint   item=epsonHorizontalPosition label="Horizontal Position"
+        Selection  item=epsonBackground         label="Background"
+    }
+    Frame label="Flip Projection" {
+        Switch  item=epsonVerticalReverse   label="Vertical Reverse"
+        Switch  item=epsonHorizontalReverse label="Horizontal Reverse"
+    }
+    Frame label="Info" {
+        Text  item=epsonAspectRatio
+        Text  item=epsonColorMode
+        Text  item=epsonColorTemperature    label="Color Temperature"
+        Text  item=epsonFleshTemperature    label="Flesh Temperature"
+        Text  item=epsonGamma
+        Text  item=epsonLuminance
+        Text  item=epsonPowerState
+        Text  item=epsonLampTime
+        Text  item=epsonErrCode
+        Text  item=epsonErrMessage
+    }
+}
+```
diff --git a/bundles/org.openhab.binding.epsonprojector/pom.xml b/bundles/org.openhab.binding.epsonprojector/pom.xml
new file mode 100644 (file)
index 0000000..48625fa
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.epsonprojector</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Epson Projector Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/feature/feature.xml b/bundles/org.openhab.binding.epsonprojector/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..06f37bf
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.epsonprojector-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-epsonprojector" description="Epson Projector Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <feature>openhab-transport-serial</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.epsonprojector/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorBindingConstants.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorBindingConstants.java
new file mode 100644 (file)
index 0000000..28439ce
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link EpsonProjectorBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Yannick Schaus - Initial contribution
+ */
+@NonNullByDefault
+public class EpsonProjectorBindingConstants {
+
+    private static final String BINDING_ID = "epsonprojector";
+
+    // 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");
+
+    // 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";
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandException.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandException.java
new file mode 100644 (file)
index 0000000..c53e143
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception for Epson projector command errors.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class EpsonProjectorCommandException extends Exception {
+
+    private static final long serialVersionUID = -8048415193494625295L;
+
+    public EpsonProjectorCommandException(String message) {
+        super(message);
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandType.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandType.java
new file mode 100644 (file)
index 0000000..774d2cc
--- /dev/null
@@ -0,0 +1,121 @@
+/**
+ * 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;
+
+import java.io.InvalidClassException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.items.Item;
+import org.openhab.core.library.items.NumberItem;
+import org.openhab.core.library.items.StringItem;
+import org.openhab.core.library.items.SwitchItem;
+
+/**
+ * Represents all valid command types which could be processed by this
+ * binding.
+ *
+ * @author Pauli Anttila - Initial contribution
+ */
+@NonNullByDefault
+public enum EpsonProjectorCommandType {
+    POWER("Power", SwitchItem.class),
+    POWER_STATE("PowerState", StringItem.class),
+    LAMP_TIME("LampTime", NumberItem.class),
+    KEY_CODE("KeyCode", NumberItem.class),
+    VKEYSTONE("VerticalKeystone", NumberItem.class),
+    HKEYSTONE("HorizontalKeystone", NumberItem.class),
+    AKEYSTONE("AutoKeystone", SwitchItem.class),
+    FREEZE("Freeze", SwitchItem.class),
+    ASPECT_RATIO("AspectRatio", StringItem.class),
+    LUMINANCE("Luminance", StringItem.class),
+    SOURCE("Source", StringItem.class),
+    BRIGHTNESS("Brightness", NumberItem.class),
+    CONTRAST("Contrast", NumberItem.class),
+    DENSITY("Density", NumberItem.class),
+    TINT("Tint", NumberItem.class),
+    COLOR_TEMP("ColorTemperature", NumberItem.class),
+    FLESH_TEMP("FleshTemperature", NumberItem.class),
+    COLOR_MODE("ColorMode", StringItem.class),
+    HPOSITION("HorizontalPosition", NumberItem.class),
+    VPOSITION("VerticalPosition", NumberItem.class),
+    GAMMA("Gamma", StringItem.class),
+    VOLUME("Volume", NumberItem.class),
+    MUTE("Mute", SwitchItem.class),
+    HREVERSE("HorizontalReverse", SwitchItem.class),
+    VREVERSE("VerticalReverse", SwitchItem.class),
+    BACKGROUND("Background", StringItem.class),
+    ERR_CODE("ErrCode", NumberItem.class),
+    ERR_MESSAGE("ErrMessage", StringItem.class),;
+
+    private final String text;
+    private Class<? extends Item> itemClass;
+
+    private EpsonProjectorCommandType(final String text, Class<? extends Item> itemClass) {
+        this.text = text;
+        this.itemClass = itemClass;
+    }
+
+    @Override
+    public String toString() {
+        return text;
+    }
+
+    public Class<? extends Item> getItemClass() {
+        return itemClass;
+    }
+
+    /**
+     * Procedure to validate command type string.
+     *
+     * @param commandTypeText
+     *            command string e.g. RawData, Command, Brightness
+     * @return true if item is valid.
+     * @throws IllegalArgumentException
+     *             Not valid command type.
+     * @throws InvalidClassException
+     *             Not valid class for command type.
+     */
+    public static boolean validateBinding(String commandTypeText, Class<? extends Item> itemClass)
+            throws IllegalArgumentException, InvalidClassException {
+        for (EpsonProjectorCommandType c : EpsonProjectorCommandType.values()) {
+            if (c.text.equalsIgnoreCase(commandTypeText)) {
+                if (c.getItemClass().equals(itemClass)) {
+                    return true;
+                } else {
+                    throw new InvalidClassException("Not valid class for command type");
+                }
+            }
+        }
+
+        throw new IllegalArgumentException("Not valid command type");
+    }
+
+    /**
+     * Procedure to convert command type string to command type class.
+     *
+     * @param commandTypeText
+     *            command string e.g. RawData, Command, Brightness
+     * @return corresponding command type.
+     * @throws InvalidClassException
+     *             Not valid class for command type.
+     */
+    public static EpsonProjectorCommandType getCommandType(String commandTypeText) throws IllegalArgumentException {
+        for (EpsonProjectorCommandType c : EpsonProjectorCommandType.values()) {
+            if (c.text.equalsIgnoreCase(commandTypeText)) {
+                return c;
+            }
+        }
+
+        throw new IllegalArgumentException("Not valid command type");
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorDevice.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorDevice.java
new file mode 100644 (file)
index 0000000..3787575
--- /dev/null
@@ -0,0 +1,626 @@
+/**
+ * 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;
+
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
+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.binding.epsonprojector.internal.configuration.EpsonProjectorConfiguration;
+import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorConnector;
+import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorSerialConnector;
+import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorTcpConnector;
+import org.openhab.binding.epsonprojector.internal.enums.AspectRatio;
+import org.openhab.binding.epsonprojector.internal.enums.Background;
+import org.openhab.binding.epsonprojector.internal.enums.ColorMode;
+import org.openhab.binding.epsonprojector.internal.enums.ErrorMessage;
+import org.openhab.binding.epsonprojector.internal.enums.Gamma;
+import org.openhab.binding.epsonprojector.internal.enums.Luminance;
+import org.openhab.binding.epsonprojector.internal.enums.PowerStatus;
+import org.openhab.binding.epsonprojector.internal.enums.Switch;
+import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provide high level interface to Epson projector.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public class EpsonProjectorDevice {
+    private static final int[] MAP64 = new int[] { 0, 3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 66,
+            70, 74, 78, 82, 86, 90, 94, 98, 102, 106, 110, 114, 118, 122, 126, 129, 133, 137, 141, 145, 149, 153, 157,
+            161, 165, 169, 173, 177, 181, 185, 189, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240,
+            244, 248, 252 };
+
+    private static final int[] MAP60 = new int[] { 0, 4, 8, 12, 16, 20, 25, 29, 33, 37, 41, 46, 50, 54, 58, 62, 67, 71,
+            75, 79, 83, 88, 92, 96, 100, 104, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 151, 155, 159, 163, 167,
+            172, 176, 180, 184, 188, 193, 197, 201, 205, 209, 214, 218, 222, 226, 230, 235, 239, 243, 247, 251 };
+
+    private static final int[] MAP49 = new int[] { 0, 5, 10, 15, 20, 25, 30, 35, 40, 46, 51, 56, 61, 66, 71, 76, 81, 87,
+            92, 97, 102, 107, 112, 117, 122, 128, 133, 138, 143, 148, 153, 158, 163, 168, 174, 179, 184, 189, 194, 199,
+            204, 209, 215, 220, 225, 230, 235, 240, 245, 250 };
+
+    private static final int[] MAP48 = new int[] { 0, 5, 10, 15, 20, 26, 31, 36, 41, 47, 52, 57, 62, 67, 73, 78, 83, 88,
+            94, 99, 104, 109, 114, 120, 125, 130, 135, 141, 146, 151, 156, 161, 167, 172, 177, 182, 188, 193, 198, 203,
+            208, 214, 219, 224, 229, 235, 240, 245, 250 };
+
+    private static final int[] MAP20 = new int[] { 0, 12, 24, 36, 48, 60, 73, 85, 97, 109, 121, 134, 146, 158, 170, 182,
+            195, 207, 219, 231, 243 };
+
+    private static final int[] MAP18 = new int[] { 0, 13, 26, 40, 53, 67, 80, 94, 107, 121, 134, 148, 161, 175, 188,
+            202, 215, 229, 242 };
+
+    private static final int[] MAP_COLOR_TEMP = new int[] { 0, 25, 51, 76, 102, 128, 153, 179, 204, 230 };
+    private static final int[] MAP_FLESH_COLOR = new int[] { 0, 36, 73, 109, 146, 182, 219 };
+
+    private static final int DEFAULT_TIMEOUT = 5 * 1000;
+    private static final int POWER_ON_TIMEOUT = 100 * 1000;
+    private static final int POWER_OFF_TIMEOUT = 130 * 1000;
+    private static final int LAMP_REFRESH_WAIT_MINUTES = 5;
+
+    private static final String ON = "ON";
+    private static final String ERR = "ERR";
+
+    private final Logger logger = LoggerFactory.getLogger(EpsonProjectorDevice.class);
+
+    private @Nullable ScheduledExecutorService scheduler = null;
+    private @Nullable ScheduledFuture<?> timeoutJob;
+
+    private EpsonProjectorConnector connection;
+    private ExpiringCache<Integer> cachedLampHours = new ExpiringCache<>(Duration.ofMinutes(LAMP_REFRESH_WAIT_MINUTES),
+            this::queryLamp);
+    private boolean connected = false;
+    private boolean ready = true;
+
+    public EpsonProjectorDevice(SerialPortManager serialPortManager, EpsonProjectorConfiguration config) {
+        connection = new EpsonProjectorSerialConnector(serialPortManager, config.serialPort);
+    }
+
+    public EpsonProjectorDevice(EpsonProjectorConfiguration config) {
+        connection = new EpsonProjectorTcpConnector(config.host, config.port);
+    }
+
+    public boolean isReady() {
+        return ready;
+    }
+
+    public void setScheduler(ScheduledExecutorService scheduler) {
+        this.scheduler = scheduler;
+    }
+
+    private synchronized @Nullable String sendQuery(String query, int timeout)
+            throws EpsonProjectorCommandException, EpsonProjectorException {
+        logger.debug("Query: '{}'", query);
+        String response = connection.sendMessage(query, timeout);
+
+        if (response.length() == 0) {
+            throw new EpsonProjectorException("No response received");
+        }
+
+        response = response.replace("\r:", "");
+        logger.debug("Response: '{}'", response);
+
+        if (ERR.equals(response)) {
+            throw new EpsonProjectorCommandException("Error response received for command: " + query);
+        }
+
+        if ("PWR OFF".equals(query) && ":".equals(response)) {
+            // When PWR OFF command is sent, next command can be sent 10 seconds after the colon is received
+            logger.debug("Refusing further commands for 10 seconds to power OFF completion");
+            ready = false;
+            ScheduledExecutorService scheduler = this.scheduler;
+            if (scheduler != null) {
+                timeoutJob = scheduler.schedule(() -> {
+                    ready = true;
+                }, 10, TimeUnit.SECONDS);
+            }
+        }
+
+        return response;
+    }
+
+    private String splitResponse(@Nullable String response) throws EpsonProjectorException {
+        if (response != null && !"".equals(response)) {
+            String[] pieces = response.split("=");
+
+            if (pieces.length < 2) {
+                throw new EpsonProjectorException("Invalid response from projector: " + response);
+            }
+
+            return pieces[1].trim();
+        } else {
+            throw new EpsonProjectorException("No response received");
+        }
+    }
+
+    protected void sendCommand(String command, int timeout)
+            throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendQuery(command, timeout);
+    }
+
+    protected void sendCommand(String command) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(command, DEFAULT_TIMEOUT);
+    }
+
+    protected int queryInt(String query, int timeout, int radix)
+            throws EpsonProjectorCommandException, EpsonProjectorException {
+        String response = sendQuery(query, timeout);
+
+        String str = splitResponse(response);
+
+        // if the response has two number groups, get the first one (Aspect Ratio does this)
+        if (str.contains(" ")) {
+            String[] subStr = str.split(" ");
+            str = subStr[0];
+        }
+
+        return Integer.parseInt(str, radix);
+    }
+
+    protected int queryInt(String query, int timeout) throws EpsonProjectorCommandException, EpsonProjectorException {
+        return queryInt(query, timeout, 10);
+    }
+
+    protected int queryInt(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
+        return queryInt(query, DEFAULT_TIMEOUT, 10);
+    }
+
+    protected int queryHexInt(String query, int timeout)
+            throws EpsonProjectorCommandException, EpsonProjectorException {
+        return queryInt(query, timeout, 16);
+    }
+
+    protected int queryHexInt(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
+        return queryInt(query, DEFAULT_TIMEOUT, 16);
+    }
+
+    protected String queryString(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
+        String response = sendQuery(query, DEFAULT_TIMEOUT);
+        return splitResponse(response);
+    }
+
+    public void connect() throws EpsonProjectorException {
+        connection.connect();
+        connected = true;
+    }
+
+    public void disconnect() throws EpsonProjectorException {
+        connection.disconnect();
+        connected = false;
+        ScheduledFuture<?> timeoutJob = this.timeoutJob;
+        if (timeoutJob != null) {
+            timeoutJob.cancel(true);
+            this.timeoutJob = null;
+        }
+    }
+
+    public boolean isConnected() {
+        return connected;
+    }
+
+    /*
+     * Power
+     */
+    public PowerStatus getPowerStatus() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int val = queryInt("PWR?");
+        return PowerStatus.forValue(val);
+    }
+
+    public void setPower(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("PWR %s", value.name()), value == Switch.ON ? POWER_ON_TIMEOUT : POWER_OFF_TIMEOUT);
+    }
+
+    /*
+     * Key code
+     */
+    public void sendKeyCode(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("KEY %02X", value));
+    }
+
+    /*
+     * Vertical Keystone
+     */
+    public int getVerticalKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int vkey = queryInt("VKEYSTONE?");
+        for (int i = 0; i < MAP60.length; i++) {
+            if (vkey == MAP60[i]) {
+                return i - 30;
+            }
+        }
+        return 0;
+    }
+
+    public void setVerticalKeystone(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 30;
+        if (value >= 0 && value <= 60) {
+            sendCommand(String.format("VKEYSTONE %d", MAP60[value]));
+        }
+    }
+
+    /*
+     * Horizontal Keystone
+     */
+    public int getHorizontalKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int hkey = queryInt("HKEYSTONE?");
+        for (int i = 0; i < MAP60.length; i++) {
+            if (hkey == MAP60[i]) {
+                return i - 30;
+            }
+        }
+        return 0;
+    }
+
+    public void setHorizontalKeystone(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 30;
+        if (value >= 0 && value <= 60) {
+            sendCommand(String.format("HKEYSTONE %d", MAP60[value]));
+        }
+    }
+
+    /*
+     * Auto Keystone
+     */
+
+    public Switch getAutoKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
+        String val = queryString("AUTOKEYSTONE?");
+        return val.equals(ON) ? Switch.ON : Switch.OFF;
+    }
+
+    public void setAutoKeystone(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("AUTOKEYSTONE %s", value.name()), DEFAULT_TIMEOUT);
+    }
+
+    /*
+     * Freeze
+     */
+    public Switch getFreeze() throws EpsonProjectorCommandException, EpsonProjectorException {
+        String val = queryString("FREEZE?");
+        return val.equals(ON) ? Switch.ON : Switch.OFF;
+    }
+
+    public void setFreeze(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("FREEZE %s", value.name()), DEFAULT_TIMEOUT);
+    }
+
+    /*
+     * Aspect Ratio
+     */
+    public AspectRatio getAspectRatio() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int val = queryHexInt("ASPECT?");
+        return AspectRatio.forValue(val);
+    }
+
+    public void setAspectRatio(AspectRatio value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("ASPECT %02X", value.toInt()));
+    }
+
+    /*
+     * Luminance
+     */
+    public Luminance getLuminance() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int val = queryHexInt("LUMINANCE?");
+        return Luminance.forValue(val);
+    }
+
+    public void setLuminance(Luminance value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("LUMINANCE %02X", value.toInt()));
+    }
+
+    /*
+     * Source
+     */
+    public String getSource() throws EpsonProjectorCommandException, EpsonProjectorException {
+        return queryString("SOURCE?");
+    }
+
+    public void setSource(String value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("SOURCE %s", value));
+    }
+
+    /*
+     * Brightness
+     */
+    public int getBrightness() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int brt = queryInt("BRIGHT?");
+        for (int i = 0; i < MAP48.length; i++) {
+            if (brt == MAP48[i]) {
+                return i - 24;
+            }
+        }
+        return 0;
+    }
+
+    public void setBrightness(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 24;
+        if (value >= 0 && value <= 48) {
+            sendCommand(String.format("BRIGHT %d", MAP48[value]));
+        }
+    }
+
+    /*
+     * Contrast
+     */
+    public int getContrast() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int con = queryInt("CONTRAST?");
+        for (int i = 0; i < MAP48.length; i++) {
+            if (con == MAP48[i]) {
+                return i - 24;
+            }
+        }
+        return 0;
+    }
+
+    public void setContrast(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 24;
+        if (value >= 0 && value <= 48) {
+            sendCommand(String.format("CONTRAST %d", MAP48[value]));
+        }
+    }
+
+    /*
+     * Density
+     */
+    public int getDensity() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int den = queryInt("DENSITY?");
+        for (int i = 0; i < MAP64.length; i++) {
+            if (den == MAP64[i]) {
+                return i - 32;
+            }
+        }
+        return 0;
+    }
+
+    public void setDensity(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 32;
+        if (value >= 0 && value <= 64) {
+            sendCommand(String.format("DENSITY %d", MAP64[value]));
+        }
+    }
+
+    /*
+     * Tint
+     */
+    public int getTint() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int tint = queryInt("TINT?");
+        for (int i = 0; i < MAP64.length; i++) {
+            if (tint == MAP64[i]) {
+                return i - 32;
+            }
+        }
+        return 0;
+    }
+
+    public void setTint(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 32;
+        if (value >= 0 && value <= 64) {
+            sendCommand(String.format("TINT %d", MAP64[value]));
+        }
+    }
+
+    /*
+     * Color Temperature
+     */
+    public int getColorTemperature() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int ctemp = queryInt("CTEMP?");
+        for (int i = 0; i < MAP_COLOR_TEMP.length; i++) {
+            if (ctemp == MAP_COLOR_TEMP[i]) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    public void setColorTemperature(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        if (value >= 0 && value <= 9) {
+            sendCommand(String.format("CTEMP %d", MAP_COLOR_TEMP[value]));
+        }
+    }
+
+    /*
+     * Flesh Color
+     */
+    public int getFleshColor() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int fclr = queryInt("FCOLOR?");
+        for (int i = 0; i < MAP_FLESH_COLOR.length; i++) {
+            if (fclr == MAP_FLESH_COLOR[i]) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    public void setFleshColor(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        if (value >= 0 && value <= 6) {
+            sendCommand(String.format("FCOLOR %d", MAP_FLESH_COLOR[value]));
+        }
+    }
+
+    /*
+     * Color Mode
+     */
+    public ColorMode getColorMode() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int val = queryHexInt("CMODE?");
+        return ColorMode.forValue(val);
+    }
+
+    public void setColorMode(ColorMode value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("CMODE %02X", value.toInt()));
+    }
+
+    /*
+     * Horizontal Position
+     */
+    public int getHorizontalPosition() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int hpos = queryInt("HPOS?");
+        for (int i = 0; i < MAP49.length; i++) {
+            if (hpos == MAP49[i]) {
+                return i - 23;
+            }
+        }
+        return 0;
+    }
+
+    public void setHorizontalPosition(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 23;
+        if (value >= 0 && value <= 49) {
+            sendCommand(String.format("HPOS %d", MAP49[value]));
+        }
+    }
+
+    /*
+     * Vertical Position
+     */
+    public int getVerticalPosition() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int vpos = queryInt("VPOS?");
+        for (int i = 0; i < MAP18.length; i++) {
+            if (vpos == MAP18[i]) {
+                return i - 8;
+            }
+        }
+        return 0;
+    }
+
+    public void setVerticalPosition(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        value = value + 8;
+        if (value >= 0 && value <= 18) {
+            sendCommand(String.format("VPOS %d", MAP18[value]));
+        }
+    }
+
+    /*
+     * Gamma
+     */
+    public Gamma getGamma() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int val = queryHexInt("GAMMA?");
+        return Gamma.forValue(val);
+    }
+
+    public void setGamma(Gamma value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("GAMMA %02X", value.toInt()));
+    }
+
+    /*
+     * Volume
+     */
+    public int getVolume() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int vol = queryInt("VOL?");
+        for (int i = 0; i < MAP20.length; i++) {
+            if (vol == MAP20[i]) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    public void setVolume(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        if (value >= 0 && value <= 20) {
+            sendCommand(String.format("VOL %d", MAP20[value]));
+        }
+    }
+
+    /*
+     * AV Mute
+     */
+    public Switch getMute() throws EpsonProjectorCommandException, EpsonProjectorException {
+        String val = queryString("MUTE?");
+        return val.equals(ON) ? Switch.ON : Switch.OFF;
+    }
+
+    public void setMute(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("MUTE %s", value.name()));
+    }
+
+    /*
+     * Horizontal Reverse
+     */
+    public Switch getHorizontalReverse() throws EpsonProjectorCommandException, EpsonProjectorException {
+        String val = queryString("HREVERSE?");
+        return val.equals(ON) ? Switch.ON : Switch.OFF;
+    }
+
+    public void setHorizontalReverse(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("HREVERSE %s", value.name()));
+    }
+
+    /*
+     * Vertical Reverse
+     */
+    public Switch getVerticalReverse() throws EpsonProjectorCommandException, EpsonProjectorException {
+        String val = queryString("VREVERSE?");
+        return val.equals(ON) ? Switch.ON : Switch.OFF;
+    }
+
+    public void setVerticalReverse(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("VREVERSE %s", value.name()));
+    }
+
+    /*
+     * Background Select for AV Mute
+     */
+    public Background getBackground() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int val = queryHexInt("MSEL?");
+        return Background.forValue(val);
+    }
+
+    public void setBackground(Background value) throws EpsonProjectorCommandException, EpsonProjectorException {
+        sendCommand(String.format("MSEL %02X", value.toInt()));
+    }
+
+    /*
+     * Lamp Time (hours) - get from cache
+     */
+    public int getLampTime() throws EpsonProjectorCommandException, EpsonProjectorException {
+        Integer lampHours = cachedLampHours.getValue();
+
+        if (lampHours != null) {
+            return lampHours.intValue();
+        } else {
+            throw new EpsonProjectorCommandException("cachedLampHours returned null");
+        }
+    }
+
+    /*
+     * Get Lamp Time
+     */
+    private @Nullable Integer queryLamp() {
+        try {
+            return Integer.valueOf(queryInt("LAMP?"));
+        } catch (EpsonProjectorCommandException | EpsonProjectorException e) {
+            logger.debug("Error executing command LAMP?", e);
+            return null;
+        }
+    }
+
+    /*
+     * Error Code
+     */
+    public int getError() throws EpsonProjectorCommandException, EpsonProjectorException {
+        return queryHexInt("ERR?");
+    }
+
+    /*
+     * Error Code Description
+     */
+    public String getErrorString() throws EpsonProjectorCommandException, EpsonProjectorException {
+        int err = queryInt("ERR?");
+        return ErrorMessage.forCode(err);
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorException.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorException.java
new file mode 100644 (file)
index 0000000..82f51df
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * 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;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Exception for Epson projector errors.
+ *
+ * @author Pauli Anttila - Initial contribution
+ */
+@NonNullByDefault
+public class EpsonProjectorException extends Exception {
+
+    private static final long serialVersionUID = -8048415193494625295L;
+
+    public EpsonProjectorException(String message) {
+        super(message);
+    }
+
+    public EpsonProjectorException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorHandlerFactory.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorHandlerFactory.java
new file mode 100644 (file)
index 0000000..815e067
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * 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;
+
+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;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link EpsonProjectorHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Yannick Schaus - Initial contribution
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@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
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Activate
+    public EpsonProjectorHandlerFactory(final @Reference SerialPortManager serialPortManager) {
+        this.serialPortManager = serialPortManager;
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (THING_TYPE_PROJECTOR_SERIAL.equals(thingTypeUID) || THING_TYPE_PROJECTOR_TCP.equals(thingTypeUID)) {
+            return new EpsonProjectorHandler(thing, serialPortManager);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/configuration/EpsonProjectorConfiguration.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/configuration/EpsonProjectorConfiguration.java
new file mode 100644 (file)
index 0000000..e34021c
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * 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.configuration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link EpsonProjectorConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Yannick Schaus - Initial contribution
+ */
+@NonNullByDefault
+public class EpsonProjectorConfiguration {
+
+    /**
+     * Serial port used for communication.
+     */
+    public String serialPort = "";
+
+    /**
+     * Host or IP address used for communication over a TCP link (if serialPort is not set).
+     */
+    public String host = "";
+
+    /**
+     * Port used for communication over a TCP link (if serialPort is not set).
+     */
+    public int port;
+
+    /**
+     * Polling interval to refresh states.
+     */
+    public int pollingInterval;
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorConnector.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorConnector.java
new file mode 100644 (file)
index 0000000..91484ad
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * 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.connector;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.epsonprojector.internal.EpsonProjectorException;
+
+/**
+ * Base class for Epson projector communication.
+ *
+ * @author Pauli Anttila - Initial contribution
+ */
+@NonNullByDefault
+public interface EpsonProjectorConnector {
+
+    /**
+     * Procedure for connecting to projector.
+     *
+     * @throws EpsonProjectorException
+     */
+    void connect() throws EpsonProjectorException;
+
+    /**
+     * Procedure for disconnecting to projector controller.
+     *
+     * @throws EpsonProjectorException
+     */
+    void disconnect() throws EpsonProjectorException;
+
+    /**
+     * Procedure for send raw data to projector.
+     *
+     * @param data
+     *            Message to send.
+     *
+     * @param timeout
+     *            timeout to wait response in milliseconds.
+     *
+     * @throws EpsonProjectorException
+     */
+    String sendMessage(String data, int timeout) throws EpsonProjectorException;
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorSerialConnector.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorSerialConnector.java
new file mode 100644 (file)
index 0000000..9523c68
--- /dev/null
@@ -0,0 +1,209 @@
+/**
+ * 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.connector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.epsonprojector.internal.EpsonProjectorException;
+import org.openhab.core.io.transport.serial.PortInUseException;
+import org.openhab.core.io.transport.serial.SerialPort;
+import org.openhab.core.io.transport.serial.SerialPortEvent;
+import org.openhab.core.io.transport.serial.SerialPortEventListener;
+import org.openhab.core.io.transport.serial.SerialPortIdentifier;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Connector for serial port communication.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public class EpsonProjectorSerialConnector implements EpsonProjectorConnector, SerialPortEventListener {
+
+    private final Logger logger = LoggerFactory.getLogger(EpsonProjectorSerialConnector.class);
+    private final String serialPortName;
+    private final SerialPortManager serialPortManager;
+
+    private @Nullable InputStream in = null;
+    private @Nullable OutputStream out = null;
+    private @Nullable SerialPort serialPort = null;
+
+    public EpsonProjectorSerialConnector(SerialPortManager serialPortManager, String serialPort) {
+        this.serialPortManager = serialPortManager;
+        this.serialPortName = serialPort;
+    }
+
+    @Override
+    public void connect() throws EpsonProjectorException {
+        try {
+            logger.debug("Open connection to serial port '{}'", serialPortName);
+
+            SerialPortIdentifier serialPortIdentifier = serialPortManager.getIdentifier(serialPortName);
+
+            if (serialPortIdentifier == null) {
+                throw new IOException("Unknown serial port");
+            }
+            SerialPort serialPort = serialPortIdentifier.open(this.getClass().getName(), 2000);
+            serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
+            serialPort.enableReceiveThreshold(1);
+            serialPort.disableReceiveTimeout();
+
+            InputStream in = serialPort.getInputStream();
+            OutputStream out = serialPort.getOutputStream();
+
+            if (in != null && out != null) {
+                out.flush();
+                if (in.markSupported()) {
+                    in.reset();
+                }
+
+                serialPort.notifyOnDataAvailable(true);
+
+                this.serialPort = serialPort;
+                this.in = in;
+                this.out = out;
+            }
+        } catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
+            throw new EpsonProjectorException(e);
+        }
+    }
+
+    @Override
+    public void disconnect() throws EpsonProjectorException {
+        InputStream in = this.in;
+        OutputStream out = this.out;
+        SerialPort serialPort = this.serialPort;
+
+        if (out != null) {
+            logger.debug("Close serial out stream");
+            try {
+                out.close();
+            } catch (IOException e) {
+                logger.debug("Error occurred when closing serial out stream: {}", e.getMessage());
+            }
+            this.out = null;
+        }
+        if (in != null) {
+            logger.debug("Close serial in stream");
+            try {
+                in.close();
+            } catch (IOException e) {
+                logger.debug("Error occurred when closing serial in stream: {}", e.getMessage());
+            }
+            this.in = null;
+        }
+        if (serialPort != null) {
+            logger.debug("Close serial port");
+            serialPort.close();
+            serialPort.removeEventListener();
+            this.serialPort = null;
+        }
+
+        logger.debug("Closed");
+    }
+
+    @Override
+    public String sendMessage(String data, int timeout) throws EpsonProjectorException {
+        InputStream in = this.in;
+        OutputStream out = this.out;
+
+        if (in == null || out == null) {
+            connect();
+            in = this.in;
+            out = this.out;
+        }
+
+        try {
+            if (in != null && out != null) {
+                // flush input stream
+                if (in.markSupported()) {
+                    in.reset();
+                } else {
+                    while (in.available() > 0) {
+                        int availableBytes = in.available();
+
+                        if (availableBytes > 0) {
+                            byte[] tmpData = new byte[availableBytes];
+                            in.read(tmpData, 0, availableBytes);
+                        }
+                    }
+                }
+                return sendMmsg(data, timeout);
+            } else {
+                return "";
+            }
+        } catch (IOException e) {
+            logger.debug("IO error occurred...reconnect and resend once: {}", e.getMessage());
+            disconnect();
+            connect();
+
+            try {
+                return sendMmsg(data, timeout);
+            } catch (IOException e1) {
+                throw new EpsonProjectorException(e);
+            }
+        }
+    }
+
+    @Override
+    public void serialEvent(SerialPortEvent arg0) {
+    }
+
+    private String sendMmsg(String data, int timeout) throws IOException, EpsonProjectorException {
+        String resp = "";
+
+        InputStream in = this.in;
+        OutputStream out = this.out;
+
+        if (in != null && out != null) {
+            out.write(data.getBytes(StandardCharsets.US_ASCII));
+            out.write("\r\n".getBytes(StandardCharsets.US_ASCII));
+            out.flush();
+
+            long startTime = System.currentTimeMillis();
+            long elapsedTime = 0;
+
+            while (elapsedTime < timeout) {
+                int availableBytes = in.available();
+                if (availableBytes > 0) {
+                    byte[] tmpData = new byte[availableBytes];
+                    int readBytes = in.read(tmpData, 0, availableBytes);
+                    resp = resp.concat(new String(tmpData, 0, readBytes, StandardCharsets.US_ASCII));
+
+                    if (resp.contains(":")) {
+                        return resp;
+                    }
+                } else {
+                    try {
+                        Thread.sleep(100);
+                    } catch (InterruptedException e) {
+                        throw new EpsonProjectorException(e);
+                    }
+                }
+
+                elapsedTime = System.currentTimeMillis() - startTime;
+            }
+        }
+
+        return resp;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorTcpConnector.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorTcpConnector.java
new file mode 100644 (file)
index 0000000..aea7eef
--- /dev/null
@@ -0,0 +1,183 @@
+/**
+ * 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.connector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.epsonprojector.internal.EpsonProjectorException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Connector for TCP communication.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public class EpsonProjectorTcpConnector implements EpsonProjectorConnector {
+
+    private final Logger logger = LoggerFactory.getLogger(EpsonProjectorTcpConnector.class);
+    private final String ip;
+    private final int port;
+
+    private @Nullable Socket socket = null;
+    private @Nullable InputStream in = null;
+    private @Nullable OutputStream out = null;
+
+    public EpsonProjectorTcpConnector(String ip, int port) {
+        this.ip = ip;
+        this.port = port;
+    }
+
+    @Override
+    public void connect() throws EpsonProjectorException {
+        logger.debug("Open connection to address'{}:{}'", ip, port);
+
+        try {
+            Socket socket = new Socket(ip, port);
+            this.socket = socket;
+            in = socket.getInputStream();
+            out = socket.getOutputStream();
+        } catch (IOException e) {
+            throw new EpsonProjectorException(e);
+        }
+    }
+
+    @Override
+    public void disconnect() throws EpsonProjectorException {
+        OutputStream out = this.out;
+
+        if (out != null) {
+            logger.debug("Close tcp out stream");
+            try {
+                out.close();
+            } catch (IOException e) {
+                logger.debug("Error occurred when closing tcp out stream: {}", e.getMessage());
+            }
+        }
+
+        InputStream in = this.in;
+        if (in != null) {
+            logger.debug("Close tcp in stream");
+            try {
+                in.close();
+            } catch (IOException e) {
+                logger.debug("Error occurred when closing tcp in stream: {}", e.getMessage());
+            }
+        }
+
+        Socket socket = this.socket;
+        if (socket != null) {
+            logger.debug("Closing socket");
+            try {
+                socket.close();
+            } catch (IOException e) {
+                logger.debug("Error occurred when closing tcp socket: {}", e.getMessage());
+            }
+        }
+
+        this.socket = null;
+        this.out = null;
+        this.in = null;
+
+        logger.debug("Closed");
+    }
+
+    @Override
+    public String sendMessage(String data, int timeout) throws EpsonProjectorException {
+        InputStream in = this.in;
+        OutputStream out = this.out;
+
+        if (in == null || out == null) {
+            connect();
+            in = this.in;
+            out = this.out;
+        }
+
+        try {
+            if (in != null) {
+                // flush input stream
+                if (in.markSupported()) {
+                    in.reset();
+                } else {
+                    while (in.available() > 0) {
+                        int availableBytes = in.available();
+
+                        if (availableBytes > 0) {
+                            byte[] tmpData = new byte[availableBytes];
+                            in.read(tmpData, 0, availableBytes);
+                        }
+                    }
+                }
+                return sendMmsg(data, timeout);
+            } else {
+                return "";
+            }
+        } catch (IOException e) {
+            logger.debug("IO error occurred...reconnect and resend once: {}", e.getMessage());
+            disconnect();
+            connect();
+
+            try {
+                return sendMmsg(data, timeout);
+            } catch (IOException e1) {
+                throw new EpsonProjectorException(e);
+            }
+        }
+    }
+
+    private String sendMmsg(String data, int timeout) throws IOException, EpsonProjectorException {
+        String resp = "";
+
+        InputStream in = this.in;
+        OutputStream out = this.out;
+
+        if (in != null && out != null) {
+            out.write(data.getBytes(StandardCharsets.US_ASCII));
+            out.write("\r\n".getBytes(StandardCharsets.US_ASCII));
+            out.flush();
+
+            long startTime = System.currentTimeMillis();
+            long elapsedTime = 0;
+
+            while (elapsedTime < timeout) {
+                int availableBytes = in.available();
+                if (availableBytes > 0) {
+                    byte[] tmpData = new byte[availableBytes];
+                    int readBytes = in.read(tmpData, 0, availableBytes);
+                    resp = resp.concat(new String(tmpData, 0, readBytes, StandardCharsets.US_ASCII));
+
+                    if (resp.contains(":")) {
+                        return resp;
+                    }
+                } else {
+                    try {
+                        Thread.sleep(100);
+                    } catch (InterruptedException e) {
+                        throw new EpsonProjectorException(e);
+                    }
+                }
+
+                elapsedTime = System.currentTimeMillis() - startTime;
+            }
+        }
+        return resp;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/AspectRatio.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/AspectRatio.java
new file mode 100644 (file)
index 0000000..3035931
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for AspectRatio.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum AspectRatio {
+    NORMAL(0x00),
+    RATIO4X3(0x10),
+    ZOOM4X3(0x12),
+    RATIO16X9(0x20),
+    UP16X9(0x21),
+    DOWN16X9(0x22),
+    AUTO(0x30),
+    FULL(0x40),
+    ZOOM(0x50),
+    REAL(0x60),
+    WIDE(0x70),
+    ANAMORPHIC(0x80),
+    SQUEEZE(0x90),
+    UNKNOWN(0xFF);
+
+    private final int value;
+
+    AspectRatio(int value) {
+        this.value = value;
+    }
+
+    public static AspectRatio forValue(int value) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.value == value).findFirst().get();
+        } catch (NoSuchElementException e) {
+            return UNKNOWN;
+        }
+    }
+
+    public int toInt() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Background.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Background.java
new file mode 100644 (file)
index 0000000..c38654a
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for Background.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum Background {
+    BLACK(0x00),
+    BLUE(0x01),
+    LOGO(0x02),
+    UNKNOWN(0xFF);
+
+    private final int value;
+
+    Background(int value) {
+        this.value = value;
+    }
+
+    public static Background forValue(int value) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.value == value).findFirst().get();
+        } catch (NoSuchElementException e) {
+            return UNKNOWN;
+        }
+    }
+
+    public int toInt() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ColorMode.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ColorMode.java
new file mode 100644 (file)
index 0000000..96bbe66
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for ColorMode.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum ColorMode {
+    SRGB(0x01),
+    NORMAL(0x02),
+    MEETING(0x03),
+    PRESENTATION(0x04),
+    CINEMANIGHT(0x05),
+    DYNAMIC(0x06),
+    NATURAL(0x07),
+    SPORTS(0x08),
+    HD(0x09),
+    CUSTOM(0x10),
+    BLACKBOARD(0x11),
+    WHITEBOARD(0x12),
+    THX(0x13),
+    PHOTO(0x14),
+    CINEMA(0x15),
+    UNKNOWN16(0x16),
+    CINEMA3D(0x17),
+    DYNAMIC3D(0x18),
+    THX3D(0x19),
+    BWCINEMA(0x20),
+    UNKNOWN21(0x21),
+    DIGITALCINEMA(0x22),
+    SILVER(0x0A),
+    XVCOLOR(0x0B),
+    LIVINGROOM(0x0C),
+    DICOMSIM(0x0F),
+    UNKNOWN(0xFF);
+
+    private final int value;
+
+    ColorMode(int value) {
+        this.value = value;
+    }
+
+    public static ColorMode forValue(int value) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.value == value).findFirst().get();
+        } catch (NoSuchElementException e) {
+            return UNKNOWN;
+        }
+    }
+
+    public int toInt() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ErrorMessage.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ErrorMessage.java
new file mode 100644 (file)
index 0000000..04d7896
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Messages for documented error codes.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum ErrorMessage {
+    NO_ERROR(0, "No error"),
+    ERROR1(1, "Fan error"),
+    ERROR3(3, "Lamp failure at power on"),
+    ERROR4(4, "High internal temperature error"),
+    ERROR6(6, "Lamp error"),
+    ERROR7(7, "Open Lamp cover door error"),
+    ERROR8(8, "Cinema filter error"),
+    ERROR9(9, "Electric dual-layered capacitor is disconnected"),
+    ERROR10(10, "Auto iris error"),
+    ERROR11(11, "Subsystem error"),
+    ERROR12(12, "Low air flow error"),
+    ERROR13(13, "Air filter air flow sensor error"),
+    ERROR14(14, "Power supply unit error (ballast)"),
+    ERROR15(15, "Shutter error"),
+    ERROR16(16, "Cooling system error (peltier element)"),
+    ERROR17(17, "Cooling system error (pump)"),
+    ERROR18(18, "Static iris error"),
+    ERROR19(19, "Power supply unit error (disagreement of ballast)"),
+    ERROR20(20, "Exhaust shutter error"),
+    ERROR21(21, "Obstacle detection error"),
+    ERROR22(22, "IF board discernment error");
+
+    private final int code;
+    private final String message;
+
+    ErrorMessage(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public static String forCode(int code) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.code == code).findFirst().get().getMessage();
+        } catch (NoSuchElementException e) {
+            return "Unknown error code: " + code;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Gamma.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Gamma.java
new file mode 100644 (file)
index 0000000..cef7c0d
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for Gamma.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum Gamma {
+    G2_0(0x20),
+    G2_1(0x21),
+    G2_2(0x22),
+    G2_3(0x23),
+    G2_4(0x24),
+    CUSTOM(0xF0),
+    UNKNOWN(0xFF);
+
+    private final int value;
+
+    Gamma(int value) {
+        this.value = value;
+    }
+
+    public static Gamma forValue(int value) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.value == value).findFirst().get();
+        } catch (NoSuchElementException e) {
+            return UNKNOWN;
+        }
+    }
+
+    public int toInt() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Luminance.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Luminance.java
new file mode 100644 (file)
index 0000000..9419f28
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for Luminance.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum Luminance {
+    NORMAL(0x00),
+    ECO(0x01),
+    MEDIUM(0x02),
+    UNKNOWN(0xFF);
+
+    private final int value;
+
+    Luminance(int value) {
+        this.value = value;
+    }
+
+    public static Luminance forValue(int value) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.value == value).findFirst().get();
+        } catch (NoSuchElementException e) {
+            return UNKNOWN;
+        }
+    }
+
+    public int toInt() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/PowerStatus.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/PowerStatus.java
new file mode 100644 (file)
index 0000000..6e77451
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * 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.enums;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for PowerStatus.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public enum PowerStatus {
+    STANDBY(0x00),
+    ON(0x01),
+    WARMUP(0x02),
+    COOLDOWN(0x03),
+    STANDBYNETWORKON(0x04),
+    ABNORMALSTANDBY(0x05),
+    UNKNOWN(0xFF);
+
+    private final int value;
+
+    PowerStatus(int value) {
+        this.value = value;
+    }
+
+    public static PowerStatus forValue(int value) {
+        try {
+            return Arrays.stream(values()).filter(e -> e.value == value).findFirst().get();
+        } catch (NoSuchElementException e) {
+            return UNKNOWN;
+        }
+    }
+
+    public int toInt() {
+        return value;
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Switch.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Switch.java
new file mode 100644 (file)
index 0000000..4bc16f6
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * 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.enums;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Valid values for Epson switch commands.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Yannick Schaus - Refactoring
+ */
+@NonNullByDefault
+public enum Switch {
+    ON,
+    OFF;
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java
new file mode 100644 (file)
index 0000000..f1c91b7
--- /dev/null
@@ -0,0 +1,417 @@
+/**
+ * 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.handler;
+
+import static org.openhab.binding.epsonprojector.internal.EpsonProjectorBindingConstants.*;
+
+import java.util.List;
+import java.util.Optional;
+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.binding.epsonprojector.internal.EpsonProjectorCommandException;
+import org.openhab.binding.epsonprojector.internal.EpsonProjectorCommandType;
+import org.openhab.binding.epsonprojector.internal.EpsonProjectorDevice;
+import org.openhab.binding.epsonprojector.internal.EpsonProjectorException;
+import org.openhab.binding.epsonprojector.internal.configuration.EpsonProjectorConfiguration;
+import org.openhab.binding.epsonprojector.internal.enums.AspectRatio;
+import org.openhab.binding.epsonprojector.internal.enums.Background;
+import org.openhab.binding.epsonprojector.internal.enums.ColorMode;
+import org.openhab.binding.epsonprojector.internal.enums.Gamma;
+import org.openhab.binding.epsonprojector.internal.enums.Luminance;
+import org.openhab.binding.epsonprojector.internal.enums.PowerStatus;
+import org.openhab.binding.epsonprojector.internal.enums.Switch;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link EpsonProjectorHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Pauli Anttila, Yannick Schaus - Initial contribution
+ * @author Michael Lobstein - Improvements for OH3
+ */
+@NonNullByDefault
+public class EpsonProjectorHandler extends BaseThingHandler {
+    private static final int DEFAULT_POLLING_INTERVAL_SEC = 10;
+
+    private final Logger logger = LoggerFactory.getLogger(EpsonProjectorHandler.class);
+    private final SerialPortManager serialPortManager;
+
+    private @Nullable ScheduledFuture<?> pollingJob;
+    private Optional<EpsonProjectorDevice> device = Optional.empty();
+
+    private boolean isPowerOn = false;
+    private int pollingInterval = DEFAULT_POLLING_INTERVAL_SEC;
+
+    public EpsonProjectorHandler(Thing thing, SerialPortManager serialPortManager) {
+        super(thing);
+        this.serialPortManager = serialPortManager;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        String channelId = channelUID.getId();
+        if (command instanceof RefreshType) {
+            Channel channel = this.thing.getChannel(channelUID);
+            if (channel != null) {
+                updateChannelState(channel);
+            }
+        } else {
+            EpsonProjectorCommandType epsonCommand = EpsonProjectorCommandType.getCommandType(channelId);
+            sendDataToDevice(epsonCommand, command);
+        }
+    }
+
+    @Override
+    public void initialize() {
+        EpsonProjectorConfiguration config = getConfigAs(EpsonProjectorConfiguration.class);
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (THING_TYPE_PROJECTOR_SERIAL.equals(thingTypeUID)) {
+            device = Optional.of(new EpsonProjectorDevice(serialPortManager, config));
+        } else if (THING_TYPE_PROJECTOR_TCP.equals(thingTypeUID)) {
+            device = Optional.of(new EpsonProjectorDevice(config));
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
+        }
+
+        pollingInterval = config.pollingInterval;
+        device.ifPresent(dev -> dev.setScheduler(scheduler));
+        updateStatus(ThingStatus.UNKNOWN);
+        schedulePollingJob();
+    }
+
+    /**
+     * Schedule the polling job
+     */
+    private void schedulePollingJob() {
+        cancelPollingJob();
+
+        pollingJob = scheduler.scheduleWithFixedDelay(() -> {
+            List<Channel> channels = this.thing.getChannels();
+            for (Channel channel : channels) {
+                // only query power & lamp time when projector is off
+                if (isPowerOn || (channel.getUID().getId().equals(CHANNEL_TYPE_POWER)
+                        || channel.getUID().getId().equals(CHANNEL_TYPE_LAMPTIME))) {
+                    updateChannelState(channel);
+                }
+            }
+        }, 0, (pollingInterval > 0) ? pollingInterval : DEFAULT_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Cancel the polling job
+     */
+    private void cancelPollingJob() {
+        ScheduledFuture<?> pollingJob = this.pollingJob;
+        if (pollingJob != null) {
+            pollingJob.cancel(true);
+            this.pollingJob = null;
+        }
+    }
+
+    @Override
+    public void dispose() {
+        cancelPollingJob();
+        closeConnection();
+        super.dispose();
+    }
+
+    private void updateChannelState(Channel channel) {
+        try {
+            if (!isLinked(channel.getUID()) && !channel.getUID().getId().equals(CHANNEL_TYPE_POWER)) {
+                return;
+            }
+
+            EpsonProjectorCommandType epsonCommand = EpsonProjectorCommandType.getCommandType(channel.getUID().getId());
+
+            State state = queryDataFromDevice(epsonCommand);
+
+            if (state != null) {
+                updateStatus(ThingStatus.ONLINE);
+                if (isLinked(channel.getUID())) {
+                    updateState(channel.getUID(), state);
+                }
+            }
+        } catch (IllegalArgumentException e) {
+            logger.warn("Unknown channel {}", channel.getUID().getId());
+        }
+    }
+
+    @Nullable
+    private State queryDataFromDevice(EpsonProjectorCommandType commandType) {
+        EpsonProjectorDevice remoteController = device.get();
+
+        try {
+            if (!remoteController.isConnected()) {
+                remoteController.connect();
+            }
+
+            if (!remoteController.isReady()) {
+                logger.debug("Refusing command {} while not ready", commandType.toString());
+                return null;
+            }
+
+            switch (commandType) {
+                case AKEYSTONE:
+                    Switch autoKeystone = remoteController.getAutoKeystone();
+                    return autoKeystone == Switch.ON ? OnOffType.ON : OnOffType.OFF;
+                case ASPECT_RATIO:
+                    AspectRatio aspectRatio = remoteController.getAspectRatio();
+                    return new StringType(aspectRatio.toString());
+                case BACKGROUND:
+                    Background background = remoteController.getBackground();
+                    return new StringType(background.toString());
+                case BRIGHTNESS:
+                    int brightness = remoteController.getBrightness();
+                    return new DecimalType(brightness);
+                case COLOR_MODE:
+                    ColorMode colorMode = remoteController.getColorMode();
+                    return new StringType(colorMode.toString());
+                case COLOR_TEMP:
+                    int ctemp = remoteController.getColorTemperature();
+                    return new DecimalType(ctemp);
+                case CONTRAST:
+                    int contrast = remoteController.getContrast();
+                    return new DecimalType(contrast);
+                case DENSITY:
+                    int density = remoteController.getDensity();
+                    return new DecimalType(density);
+                case ERR_CODE:
+                    int err = remoteController.getError();
+                    return new DecimalType(err);
+                case ERR_MESSAGE:
+                    String errString = remoteController.getErrorString();
+                    return new StringType(errString);
+                case FLESH_TEMP:
+                    int fleshColor = remoteController.getFleshColor();
+                    return new DecimalType(fleshColor);
+                case FREEZE:
+                    Switch freeze = remoteController.getFreeze();
+                    return freeze == Switch.ON ? OnOffType.ON : OnOffType.OFF;
+                case GAMMA:
+                    Gamma gamma = remoteController.getGamma();
+                    return new StringType(gamma.toString());
+                case HKEYSTONE:
+                    int hKeystone = remoteController.getHorizontalKeystone();
+                    return new DecimalType(hKeystone);
+                case HPOSITION:
+                    int hPosition = remoteController.getHorizontalPosition();
+                    return new DecimalType(hPosition);
+                case HREVERSE:
+                    Switch hReverse = remoteController.getHorizontalReverse();
+                    return hReverse == Switch.ON ? OnOffType.ON : OnOffType.OFF;
+                case KEY_CODE:
+                    break;
+                case LAMP_TIME:
+                    int lampTime = remoteController.getLampTime();
+                    return new DecimalType(lampTime);
+                case LUMINANCE:
+                    Luminance luminance = remoteController.getLuminance();
+                    return new StringType(luminance.toString());
+                case MUTE:
+                    Switch mute = remoteController.getMute();
+                    return mute == Switch.ON ? OnOffType.ON : OnOffType.OFF;
+                case POWER:
+                    PowerStatus powerStatus = remoteController.getPowerStatus();
+                    if (isLinked(CHANNEL_TYPE_POWERSTATE)) {
+                        updateState(CHANNEL_TYPE_POWERSTATE, new StringType(powerStatus.toString()));
+                    }
+
+                    if (powerStatus == PowerStatus.ON || powerStatus == PowerStatus.WARMUP) {
+                        isPowerOn = true;
+                        return OnOffType.ON;
+                    } else {
+                        isPowerOn = false;
+                        return OnOffType.OFF;
+                    }
+                case POWER_STATE:
+                    return null;
+                case SOURCE:
+                    return new StringType(remoteController.getSource());
+                case TINT:
+                    int tint = remoteController.getTint();
+                    return new DecimalType(tint);
+                case VKEYSTONE:
+                    int vKeystone = remoteController.getVerticalKeystone();
+                    return new DecimalType(vKeystone);
+                case VOLUME:
+                    int volume = remoteController.getVolume();
+                    return new DecimalType(volume);
+                case VPOSITION:
+                    int vPosition = remoteController.getVerticalPosition();
+                    return new DecimalType(vPosition);
+                case VREVERSE:
+                    Switch vReverse = remoteController.getVerticalReverse();
+                    return vReverse == Switch.ON ? OnOffType.ON : OnOffType.OFF;
+                default:
+                    logger.warn("Unknown '{}' command!", commandType);
+                    return UnDefType.UNDEF;
+            }
+        } catch (EpsonProjectorCommandException e) {
+            logger.debug("Error executing command '{}', {}", commandType, e.getMessage());
+            return UnDefType.UNDEF;
+        } catch (EpsonProjectorException e) {
+            logger.debug("Couldn't execute command '{}', {}", commandType, e.getMessage());
+            closeConnection();
+            return null;
+        }
+
+        return UnDefType.UNDEF;
+    }
+
+    private void sendDataToDevice(EpsonProjectorCommandType commandType, Command command) {
+        EpsonProjectorDevice remoteController = device.get();
+
+        try {
+            if (!remoteController.isConnected()) {
+                remoteController.connect();
+            }
+
+            if (!remoteController.isReady()) {
+                logger.debug("Refusing command '{}' while not ready", commandType.toString());
+                return;
+            }
+
+            switch (commandType) {
+                case AKEYSTONE:
+                    remoteController.setAutoKeystone((command == OnOffType.ON ? Switch.ON : Switch.OFF));
+                    break;
+                case ASPECT_RATIO:
+                    remoteController.setAspectRatio(AspectRatio.valueOf(command.toString()));
+                    break;
+                case BACKGROUND:
+                    remoteController.setBackground(Background.valueOf(command.toString()));
+                    break;
+                case BRIGHTNESS:
+                    remoteController.setBrightness(((DecimalType) command).intValue());
+                    break;
+                case COLOR_MODE:
+                    remoteController.setColorMode(ColorMode.valueOf(command.toString()));
+                    break;
+                case COLOR_TEMP:
+                    remoteController.setColorTemperature(((DecimalType) command).intValue());
+                    break;
+                case CONTRAST:
+                    remoteController.setContrast(((DecimalType) command).intValue());
+                    break;
+                case DENSITY:
+                    remoteController.setDensity(((DecimalType) command).intValue());
+                    break;
+                case ERR_CODE:
+                    logger.warn("'{}' is read only parameter", commandType);
+                    break;
+                case ERR_MESSAGE:
+                    logger.warn("'{}' is read only parameter", commandType);
+                    break;
+                case FLESH_TEMP:
+                    remoteController.setFleshColor(((DecimalType) command).intValue());
+                    break;
+                case FREEZE:
+                    remoteController.setFreeze(command == OnOffType.ON ? Switch.ON : Switch.OFF);
+                    break;
+                case GAMMA:
+                    remoteController.setGamma(Gamma.valueOf(command.toString()));
+                    break;
+                case HKEYSTONE:
+                    remoteController.setHorizontalKeystone(((DecimalType) command).intValue());
+                    break;
+                case HPOSITION:
+                    remoteController.setHorizontalPosition(((DecimalType) command).intValue());
+                    break;
+                case HREVERSE:
+                    remoteController.setHorizontalReverse((command == OnOffType.ON ? Switch.ON : Switch.OFF));
+                    break;
+                case KEY_CODE:
+                    remoteController.sendKeyCode(((DecimalType) command).intValue());
+                    break;
+                case LAMP_TIME:
+                    logger.warn("'{}' is read only parameter", commandType);
+                    break;
+                case LUMINANCE:
+                    remoteController.setLuminance(Luminance.valueOf(command.toString()));
+                    break;
+                case MUTE:
+                    remoteController.setMute((command == OnOffType.ON ? Switch.ON : Switch.OFF));
+                    break;
+                case POWER:
+                    if (command == OnOffType.ON) {
+                        remoteController.setPower(Switch.ON);
+                        isPowerOn = true;
+                    } else {
+                        remoteController.setPower(Switch.OFF);
+                        isPowerOn = false;
+                    }
+                    break;
+                case POWER_STATE:
+                    logger.warn("'{}' is read only parameter", commandType);
+                    break;
+                case SOURCE:
+                    remoteController.setSource(command.toString());
+                    break;
+                case TINT:
+                    remoteController.setTint(((DecimalType) command).intValue());
+                    break;
+                case VKEYSTONE:
+                    remoteController.setVerticalKeystone(((DecimalType) command).intValue());
+                    break;
+                case VOLUME:
+                    remoteController.setVolume(((DecimalType) command).intValue());
+                    break;
+                case VPOSITION:
+                    remoteController.setVerticalPosition(((DecimalType) command).intValue());
+                    break;
+                case VREVERSE:
+                    remoteController.setVerticalReverse((command == OnOffType.ON ? Switch.ON : Switch.OFF));
+                    break;
+                default:
+                    logger.warn("Unknown '{}' command!", commandType);
+                    break;
+            }
+        } catch (EpsonProjectorCommandException e) {
+            logger.debug("Error executing command '{}', {}", commandType, e.getMessage());
+        } catch (EpsonProjectorException e) {
+            logger.warn("Couldn't execute command '{}', {}", commandType, e.getMessage());
+            closeConnection();
+        }
+    }
+
+    private void closeConnection() {
+        EpsonProjectorDevice remoteController = device.get();
+        try {
+            logger.debug("Closing connection to device '{}'", this.thing.getUID());
+            remoteController.disconnect();
+            updateStatus(ThingStatus.OFFLINE);
+        } catch (EpsonProjectorException e) {
+            logger.debug("Error occurred when closing connection to device '{}'", this.thing.getUID(), e);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..604294c
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="epsonprojector" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Epson Projector Binding</name>
+       <description>This binding is compatible with Epson projectors which support the ESC/VP21 protocol</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..e4302be
--- /dev/null
@@ -0,0 +1,346 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="epsonprojector"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="projector-serial">
+               <label>Epson Projector - Serial</label>
+               <description>An Epson projector which supports the ESC/VP21 protocol via a serial port connection</description>
+
+               <channels>
+                       <channel id="power" typeId="power"/>
+                       <channel id="powerstate" typeId="powerstate"/>
+                       <channel id="source" typeId="source"/>
+                       <channel id="aspectratio" typeId="aspectratio"/>
+                       <channel id="colormode" typeId="colormode"/>
+                       <channel id="freeze" typeId="freeze"/>
+                       <channel id="mute" typeId="mute"/>
+                       <channel id="volume" typeId="volume"/>
+                       <channel id="luminance" typeId="luminance"/>
+                       <channel id="brightness" typeId="brightness"/>
+                       <channel id="contrast" typeId="contrast"/>
+                       <channel id="density" typeId="density"/>
+                       <channel id="tint" typeId="tint"/>
+                       <channel id="colortemperature" typeId="colortemperature"/>
+                       <channel id="fleshtemperature" typeId="fleshtemperature"/>
+                       <channel id="gamma" typeId="gamma"/>
+                       <channel id="autokeystone" typeId="autokeystone"/>
+                       <channel id="verticalkeystone" typeId="verticalkeystone"/>
+                       <channel id="horizontalkeystone" typeId="horizontalkeystone"/>
+                       <channel id="verticalposition" typeId="verticalposition"/>
+                       <channel id="horizontalposition" typeId="horizontalposition"/>
+                       <channel id="verticalreverse" typeId="verticalreverse"/>
+                       <channel id="horizontalreverse" typeId="horizontalreverse"/>
+                       <channel id="background" typeId="background"/>
+                       <channel id="keycode" typeId="keycode"/>
+                       <channel id="lamptime" typeId="lamptime"/>
+                       <channel id="errcode" typeId="errcode"/>
+                       <channel id="errmessage" typeId="errmessage"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="serialPort" type="text" required="true">
+                               <label>Serial Port</label>
+                               <context>serial-port</context>
+                               <description>Serial Port to Use for Connecting to the Epson Projector</description>
+                       </parameter>
+                       <parameter name="pollingInterval" type="integer" min="5" max="60" unit="s" required="false">
+                               <label>Polling interval</label>
+                               <description>Configures How Often to Poll the Projector for Updates (5-60; Default 10)</description>
+                               <default>10</default>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <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>
+
+               <channels>
+                       <channel id="power" typeId="power"/>
+                       <channel id="powerstate" typeId="powerstate"/>
+                       <channel id="source" typeId="source"/>
+                       <channel id="aspectratio" typeId="aspectratio"/>
+                       <channel id="colormode" typeId="colormode"/>
+                       <channel id="freeze" typeId="freeze"/>
+                       <channel id="mute" typeId="mute"/>
+                       <channel id="volume" typeId="volume"/>
+                       <channel id="luminance" typeId="luminance"/>
+                       <channel id="brightness" typeId="brightness"/>
+                       <channel id="contrast" typeId="contrast"/>
+                       <channel id="density" typeId="density"/>
+                       <channel id="tint" typeId="tint"/>
+                       <channel id="colortemperature" typeId="colortemperature"/>
+                       <channel id="fleshtemperature" typeId="fleshtemperature"/>
+                       <channel id="gamma" typeId="gamma"/>
+                       <channel id="autokeystone" typeId="autokeystone"/>
+                       <channel id="verticalkeystone" typeId="verticalkeystone"/>
+                       <channel id="horizontalkeystone" typeId="horizontalkeystone"/>
+                       <channel id="verticalposition" typeId="verticalposition"/>
+                       <channel id="horizontalposition" typeId="horizontalposition"/>
+                       <channel id="verticalreverse" typeId="verticalreverse"/>
+                       <channel id="horizontalreverse" typeId="horizontalreverse"/>
+                       <channel id="background" typeId="background"/>
+                       <channel id="keycode" typeId="keycode"/>
+                       <channel id="lamptime" typeId="lamptime"/>
+                       <channel id="errcode" typeId="errcode"/>
+                       <channel id="errmessage" typeId="errmessage"/>
+               </channels>
+
+               <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>
+                       </parameter>
+                       <parameter name="port" type="integer" min="1" max="65535" required="true">
+                               <label>Port</label>
+                               <description>Port for the serial over IP device</description>
+                       </parameter>
+                       <parameter name="pollingInterval" type="integer" min="5" max="60" unit="s" required="false">
+                               <label>Polling interval</label>
+                               <description>Configures How Often to Poll the Projector for Updates (5-60; Default 10)</description>
+                               <default>10</default>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <channel-type id="power">
+               <item-type>Switch</item-type>
+               <label>Power</label>
+               <description>Powers the Projector On or Off</description>
+       </channel-type>
+       <channel-type id="powerstate">
+               <item-type>String</item-type>
+               <label>Power State</label>
+               <description>Retrieves the Textual Power State of the Projector</description>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="lamptime">
+               <item-type>Number</item-type>
+               <label>Lamp Time</label>
+               <description>Retrieves the Lamp Hours</description>
+               <state readOnly="true" pattern="%d h"/>
+       </channel-type>
+       <channel-type id="keycode" advanced="true">
+               <item-type>Number</item-type>
+               <label>KeyCode</label>
+               <description>Send a KEY Operation Command to the Projector</description>
+       </channel-type>
+       <channel-type id="verticalkeystone" advanced="true">
+               <item-type>Number</item-type>
+               <label>Vertical Keystone</label>
+               <description>Retrieve or Set the Vertical Keystone</description>
+               <state min="-30" max="30" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="horizontalkeystone" advanced="true">
+               <item-type>Number</item-type>
+               <label>Horizontal Keystone</label>
+               <description>Retrieve or Set the Horizontal Keystone</description>
+               <state min="-30" max="30" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="autokeystone" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Auto Keystone</label>
+               <description>Turn the Auto Keystone On or Off</description>
+       </channel-type>
+       <channel-type id="freeze">
+               <item-type>Switch</item-type>
+               <label>Freeze Image</label>
+               <description>Turn the Freeze Screen Mode On or Off</description>
+       </channel-type>
+       <channel-type id="aspectratio">
+               <item-type>String</item-type>
+               <label>Aspect Ratio</label>
+               <description>Retrieve or Set the Aspect Ratio</description>
+               <state>
+                       <options>
+                               <option value="NORMAL">NORMAL</option>
+                               <option value="AUTO">AUTO</option>
+                               <option value="FULL">FULL</option>
+                               <option value="ZOOM">ZOOM</option>
+                               <option value="WIDE">WIDE</option>
+                               <option value="ANAMORPHIC">ANAMORPHIC</option>
+                               <option value="SQUEEZE">SQUEEZE</option>
+                               <option value="RATIO4X3">RATIO4X3</option>
+                               <option value="ZOOM4X3">ZOOM4X3</option>
+                               <option value="RATIO16X9">RATIO16X9</option>
+                               <option value="UP16X9">UP16X9</option>
+                               <option value="DOWN16X9">DOWN16X9</option>
+                               <option value="REAL">REAL</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="luminance">
+               <item-type>String</item-type>
+               <label>Luminance</label>
+               <description>Retrieve or Set the Lamp Mode</description>
+               <state>
+                       <options>
+                               <option value="NORMAL">NORMAL</option>
+                               <option value="ECO">ECO</option>
+                               <option value="MEDIUM">MEDIUM</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="source">
+               <item-type>String</item-type>
+               <label>Source</label>
+               <description>Retrieve or Set the Input Source</description>
+               <state>
+                       <options>
+                               <option value="30">HDMI1</option>
+                               <option value="A0">HDMI2</option>
+                               <option value="14">Component</option>
+                               <option value="20">PC DSUB</option>
+                               <option value="41">Video</option>
+                               <option value="42">S-Video</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="brightness" advanced="true">
+               <item-type>Number</item-type>
+               <label>Brightness</label>
+               <description>Retrieve or Set the Brightness</description>
+               <state min="-24" max="24" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="contrast" advanced="true">
+               <item-type>Number</item-type>
+               <label>Contrast</label>
+               <description>Retrieve or Set the Contrast</description>
+               <state min="-24" max="24" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="density" advanced="true">
+               <item-type>Number</item-type>
+               <label>Density (Color Saturation)</label>
+               <description>Retrieve or Set the Density</description>
+               <state min="-32" max="32" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="tint" advanced="true">
+               <item-type>Number</item-type>
+               <label>Tint</label>
+               <description>Retrieve or Set the Tint</description>
+               <state min="-32" max="32" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="colortemperature" advanced="true">
+               <item-type>Number</item-type>
+               <label>Color Temperature</label>
+               <description>Retrieve or Set the Color Temperature</description>
+               <state min="0" max="9" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="fleshtemperature" advanced="true">
+               <item-type>Number</item-type>
+               <label>Flesh Temperature</label>
+               <description>Retrieve or Set the Flesh Temperature</description>
+               <state min="0" max="6" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="colormode">
+               <item-type>String</item-type>
+               <label>Color Mode</label>
+               <description>Retrieve or Set the Color Mode</description>
+               <state>
+                       <options>
+                               <option value="DYNAMIC">DYNAMIC</option>
+                               <option value="LIVINGROOM">LIVINGROOM</option>
+                               <option value="NATURAL">NATURAL</option>
+                               <option value="CINEMA">CINEMA</option>
+                               <option value="CINEMANIGHT">CINEMANIGHT</option>
+                               <option value="BWCINEMA">BWCINEMA</option>
+                               <option value="DIGITALCINEMA">DIGITALCINEMA</option>
+                               <option value="HD">HD</option>
+                               <option value="THX">THX</option>
+                               <option value="CINEMA3D">CINEMA3D</option>
+                               <option value="DYNAMIC3D">DYNAMIC3D</option>
+                               <option value="THX3D">THX3D</option>
+                               <option value="SRGB">SRGB</option>
+                               <option value="NORMAL">NORMAL</option>
+                               <option value="MEETING">MEETING</option>
+                               <option value="PRESENTATION">PRESENTATION</option>
+                               <option value="SPORTS">SPORTS</option>
+                               <option value="CUSTOM">CUSTOM</option>
+                               <option value="BLACKBOARD">BLACKBOARD</option>
+                               <option value="WHITEBOARD">WHITEBOARD</option>
+                               <option value="PHOTO">PHOTO</option>
+                               <option value="SILVER">SILVER</option>
+                               <option value="XVCOLOR">XVCOLOR</option>
+                               <option value="DICOMSIM">DICOMSIM</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="horizontalposition" advanced="true">
+               <item-type>Number</item-type>
+               <label>Horizontal Position</label>
+               <description>Retrieve or Set the Horizontal Position</description>
+               <state min="-23" max="26" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="verticalposition" advanced="true">
+               <item-type>Number</item-type>
+               <label>Vertical Position</label>
+               <description>Retrieve or Set the Vertical Position</description>
+               <state min="-8" max="10" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="gamma" advanced="true">
+               <item-type>String</item-type>
+               <label>Gamma</label>
+               <description>Retrieve or Set the Gamma Setting</description>
+               <state>
+                       <options>
+                               <option value="G2_0">G2_0</option>
+                               <option value="G2_1">G2_1</option>
+                               <option value="G2_2">G2_2</option>
+                               <option value="G2_3">G2_3</option>
+                               <option value="G2_4">G2_4</option>
+                               <option value="CUSTOM">CUSTOM</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="volume">
+               <item-type>Number</item-type>
+               <label>Volume</label>
+               <description>Retrieve or Set the Volume</description>
+               <state min="0" max="20" step="1" pattern="%d"/>
+       </channel-type>
+       <channel-type id="mute">
+               <item-type>Switch</item-type>
+               <label>AV Mute</label>
+               <description>Turn the AV Mute On or Off</description>
+       </channel-type>
+       <channel-type id="horizontalreverse" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Horizontal Reverse</label>
+               <description>Turn the Horizontal Reverse On or Off</description>
+       </channel-type>
+       <channel-type id="verticalreverse" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Vertical Reverse</label>
+               <description>Turn the Vertical Reverse On or Off</description>
+       </channel-type>
+       <channel-type id="background" advanced="true">
+               <item-type>String</item-type>
+               <label>Background</label>
+               <description>Select the Background Color/Logo</description>
+               <state>
+                       <options>
+                               <option value="BLACK">BLACK</option>
+                               <option value="BLUE">BLUE</option>
+                               <option value="LOGO">LOGO</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="errcode" advanced="true">
+               <item-type>Number</item-type>
+               <label>ErrCode</label>
+               <description>Retrieves the Last Error Code</description>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="errmessage" advanced="true">
+               <item-type>String</item-type>
+               <label>ErrMessage</label>
+               <description>Retrieves the Description of the Last Error</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index fa3dde1666c27589efd8a0bd1f124b8f6f4a88df..9b25b075474a6fd808b80fdafe141c39fae8bf60 100644 (file)
@@ -95,6 +95,7 @@
     <module>org.openhab.binding.enigma2</module>
     <module>org.openhab.binding.enocean</module>
     <module>org.openhab.binding.enturno</module>
+    <module>org.openhab.binding.epsonprojector</module>
     <module>org.openhab.binding.etherrain</module>
     <module>org.openhab.binding.evohome</module>
     <module>org.openhab.binding.exec</module>