diff --git a/CODEOWNERS b/CODEOWNERS
index c6329d68d026b..c9b9feb2b2632 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 5f87670cbfa05..450ff96f58122 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -306,6 +306,11 @@
org.openhab.binding.enturno
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.epsonprojector
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.etherrain
diff --git a/bundles/org.openhab.binding.epsonprojector/NOTICE b/bundles/org.openhab.binding.epsonprojector/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/NOTICE
@@ -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
index 0000000000000..e2b31391ea819
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/README.md
@@ -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]" { channel="epsonprojector:projector:hometheater:powerstate" }
+Number epsonLampTime "Lamp Time [%d h]" { 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
index 0000000000000..48625fa887148
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.0.0-SNAPSHOT
+
+
+ org.openhab.binding.epsonprojector
+
+ openHAB Add-ons :: Bundles :: Epson Projector Binding
+
+
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
index 0000000000000..06f37bff5b409
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-serial
+ mvn:org.openhab.addons.bundles/org.openhab.binding.epsonprojector/${project.version}
+
+
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
index 0000000000000..28439cec508d0
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorBindingConstants.java
@@ -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
index 0000000000000..c53e14368e844
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandException.java
@@ -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
index 0000000000000..774d2cc08526c
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorCommandType.java
@@ -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
index 0000000000000..37875759ab366
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorDevice.java
@@ -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 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
index 0000000000000..82f51df08d92d
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorException.java
@@ -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
index 0000000000000..815e06701d23c
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/EpsonProjectorHandlerFactory.java
@@ -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 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
index 0000000000000..e34021cfd3ae4
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/configuration/EpsonProjectorConfiguration.java
@@ -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
index 0000000000000..91484ad136239
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorConnector.java
@@ -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
index 0000000000000..9523c68768890
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorSerialConnector.java
@@ -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
index 0000000000000..aea7eefc4951b
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/connector/EpsonProjectorTcpConnector.java
@@ -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
index 0000000000000..30359319c7bfa
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/AspectRatio.java
@@ -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
index 0000000000000..c38654a353f55
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Background.java
@@ -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
index 0000000000000..96bbe66a3e168
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ColorMode.java
@@ -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
index 0000000000000..04d78960a6455
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/ErrorMessage.java
@@ -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
index 0000000000000..cef7c0dbfbced
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Gamma.java
@@ -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
index 0000000000000..9419f2858c5c9
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Luminance.java
@@ -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
index 0000000000000..6e77451bb6a3f
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/PowerStatus.java
@@ -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
index 0000000000000..4bc16f620663c
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/enums/Switch.java
@@ -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
index 0000000000000..f1c91b7646cc2
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/java/org/openhab/binding/epsonprojector/internal/handler/EpsonProjectorHandler.java
@@ -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 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 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
index 0000000000000..604294c098506
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Epson Projector Binding
+ This binding is compatible with Epson projectors which support the ESC/VP21 protocol
+
+
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
index 0000000000000..e4302be7b12c1
--- /dev/null
+++ b/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,346 @@
+
+
+
+
+
+ An Epson projector which supports the ESC/VP21 protocol via a serial port connection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ serial-port
+ Serial Port to Use for Connecting to the Epson Projector
+
+
+
+ Configures How Often to Poll the Projector for Updates (5-60; Default 10)
+ 10
+
+
+
+
+
+
+
+ An Epson projector which supports the ESC/VP21 protocol via a serial over IP connection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+ IP address for the serial over IP device
+
+
+
+ Port for the serial over IP device
+
+
+
+ Configures How Often to Poll the Projector for Updates (5-60; Default 10)
+ 10
+
+
+
+
+
+
+ Switch
+
+ Powers the Projector On or Off
+
+
+ String
+
+ Retrieves the Textual Power State of the Projector
+
+
+
+ Number
+
+ Retrieves the Lamp Hours
+
+
+
+ Number
+
+ Send a KEY Operation Command to the Projector
+
+
+ Number
+
+ Retrieve or Set the Vertical Keystone
+
+
+
+ Number
+
+ Retrieve or Set the Horizontal Keystone
+
+
+
+ Switch
+
+ Turn the Auto Keystone On or Off
+
+
+ Switch
+
+ Turn the Freeze Screen Mode On or Off
+
+
+ String
+
+ Retrieve or Set the Aspect Ratio
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ Retrieve or Set the Lamp Mode
+
+
+
+
+
+
+
+
+
+ String
+
+ Retrieve or Set the Input Source
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Retrieve or Set the Brightness
+
+
+
+ Number
+
+ Retrieve or Set the Contrast
+
+
+
+ Number
+
+ Retrieve or Set the Density
+
+
+
+ Number
+
+ Retrieve or Set the Tint
+
+
+
+ Number
+
+ Retrieve or Set the Color Temperature
+
+
+
+ Number
+
+ Retrieve or Set the Flesh Temperature
+
+
+
+ String
+
+ Retrieve or Set the Color Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Retrieve or Set the Horizontal Position
+
+
+
+ Number
+
+ Retrieve or Set the Vertical Position
+
+
+
+ String
+
+ Retrieve or Set the Gamma Setting
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ Retrieve or Set the Volume
+
+
+
+ Switch
+
+ Turn the AV Mute On or Off
+
+
+ Switch
+
+ Turn the Horizontal Reverse On or Off
+
+
+ Switch
+
+ Turn the Vertical Reverse On or Off
+
+
+ String
+
+ Select the Background Color/Logo
+
+
+
+
+
+
+
+
+
+ Number
+
+ Retrieves the Last Error Code
+
+
+
+ String
+
+ Retrieves the Description of the Last Error
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index fa3dde1666c27..9b25b075474a6 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -95,6 +95,7 @@
org.openhab.binding.enigma2
org.openhab.binding.enocean
org.openhab.binding.enturno
+ org.openhab.binding.epsonprojector
org.openhab.binding.etherrain
org.openhab.binding.evohome
org.openhab.binding.exec