diff --git a/CODEOWNERS b/CODEOWNERS
index e4811da7232ee..9844b02f89260 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -159,6 +159,7 @@
/bundles/org.openhab.binding.openthermgateway/ @ArjenKorevaar
/bundles/org.openhab.binding.openuv/ @clinique
/bundles/org.openhab.binding.openweathermap/ @cweitkamp
+/bundles/org.openhab.binding.oppo/ @mlobstein
/bundles/org.openhab.binding.orvibo/ @tavalin
/bundles/org.openhab.binding.paradoxalarm/ @theater
/bundles/org.openhab.binding.pentair/ @jsjames
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index c356201115f5d..b08c809bcd34b 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -784,6 +784,11 @@
org.openhab.binding.openweathermap${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.oppo
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.orvibo
diff --git a/bundles/org.openhab.binding.oppo/.classpath b/bundles/org.openhab.binding.oppo/.classpath
new file mode 100644
index 0000000000000..d223a57c7967a
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.oppo/.project b/bundles/org.openhab.binding.oppo/.project
new file mode 100644
index 0000000000000..271d20112795a
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.binding.oppo
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.binding.oppo/NOTICE b/bundles/org.openhab.binding.oppo/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/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.oppo/README.md b/bundles/org.openhab.binding.oppo/README.md
new file mode 100644
index 0000000000000..86a5f5cc9ca3c
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/README.md
@@ -0,0 +1,211 @@
+# Oppo UDP-203/205, BDP-103/105, BDP-93/95 & BDP-83 Blu-ray player Binding
+
+This binding can be used to control the Oppo UDP-203/205 or BDP-83/93/95/103/105 Blu-ray player.
+Almost all features of the various models of this player line are supported by the binding.
+This binding was tested on a BDP-103 only, so there might be issues with other models that will need to be fixed.
+Please report any issues found.
+Also review the notes below for some important usage caveats.
+
+The binding supports three different kinds of connections:
+
+* direct IP connection (with caveats),
+* serial connection,
+* serial over IP connection
+
+For users without a serial connector on server side, you can use a serial to USB adapter.
+
+You don't need to have your player device directly connected to your openHAB server.
+You can connect it for example to a Raspberry Pi and use [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) to make the serial connection available on LAN (serial over IP).
+
+## Supported Things
+
+There is exactly one supported thing type, which represents the player.
+It has the `player` id.
+
+## Discovery
+
+Manually initiated Auto-discovery is supported if the player is accessible on the same IP subnet of the openHAB server.
+In the Inbox, select Search For Things and then choose the Oppo Blu-ray Player Binding to initiate discovery.
+
+## Binding Configuration
+
+There are no overall binding configuration settings that need to be set.
+All settings are through thing configuration parameters.
+
+## Thing Configuration
+
+The thing has the following configuration parameters:
+
+| Parameter Label | Parameter ID | Description | Accepted values |
+|------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|---------------------------|
+| Player Model | model | Specifies what model of player is to be controlled by the binding (required). | 83, 103, 105, 203, or 205 |
+| Address | host | Host name or IP address of the Oppo player or serial over IP device. | host name or ip |
+| Port | port | Communication port for using serial over IP. Leave blank if using direct IP connection to the player. | ip port number |
+| Serial Port | serialPort | Serial port to use for directly connecting to the Oppo player | a comm port name |
+| Verbose Mode | verboseMode | (Optional) If true, the player will send time updates every second. If set false, the binding polls the player every 15 seconds. | Boolean; default false |
+
+Some notes:
+
+* If using direct IP connection on the BDP series (83/93/95/103/105), verbose mode is not supported.
+* For some reason on these models, the unsolicited status update messages are not generated over the IP socket.
+* If fast updates are required on these models, a direct serial or serial over IP connection to the player is required.
+* The UDP-20x series should be fully functional over direct IP connection but this was not able to be tested by the developer.
+* As previously noted, when using verbose mode, the player will send time code messages once per second while playback is ongoing.
+* Be aware that this could cause performance impacts to your openHAB system.
+* In non-verbose (the default), the binding will poll the player every 15 seconds to update play time, track and chapter information instead.
+* In order for the direct IP connection to work while the player is turned off, the Standby Mode setting must be set to "Quick Start" in the Device Setup menu.
+* Likewise if the player is turned off, it may not be discoverable by the Binding's discovery scan.
+* If you experience any issues using the binding, first ensure that the player's firmware is up to date with the latest available version (especially on the older models).
+* For the older models, some of the features in the control API were added after the players were shipped.
+* Available HDMI modes for BDP-83 & BDP-9x: AUTO, SRC, 1080P, 1080I, 720P, SDP, SDI
+* Available HDMI modes for BDP-10x: AUTO, SRC, 4K2K, 1080P, 1080I, 720P, SDP, SDI
+* Available HDMI modes for UDP-20x: AUTO, SRC, UHD_AUTO, UHD24, UHD50, UHD60, 1080P_AUTO, 1080P24, 1080P50, 1080P60, 1080I50, 1080I60, 720P50, 720P60, 567P, 567I, 480P, 480I
+* On Linux, you may get an error stating the serial port cannot be opened when the Oppo 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. Oppo 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 Oppo player):
+
+```
+4444:raw:0:/dev/ttyUSB0:9600 8DATABITS NONE 1STOPBIT LOCAL
+```
+
+## Channels
+
+The following channels are available:
+
+| Channel ID | Item Type | Description |
+|-------------------|-------------|----------------------------------------------------------------------------------------------------------------|
+| power | Switch | Turn the power for the player on or off |
+| volume | Dimmer | Control the volume for the player (0-100%) |
+| mute | Switch | Mute or unmute the volume on the player |
+| source | Number | Select the source input for the player (0-6; number of available options varies by model) |
+| play_mode | String | Indicates the current playback mode of the player (ReadOnly) |
+| control | Player | Simulate pressing the transport control buttons on the remote control (play/pause/next/previous/rew/ffwd) |
+| time_mode | String | Sets the time information display mode on the player (T, X, C, K) |
+| time_display | Number:Time | The playback time elapsed/remaining in seconds (ReadOnly) |
+| current_title | Number | The current title or track number playing (ReadOnly) |
+| total_title | Number | The total number of titles or tracks on the disc (ReadOnly) |
+| current_chapter | Number | The current chapter number player (ReadOnly) |
+| total_chapter | Number | The total number of chapters in the current title (ReadOnly) |
+| repeat_mode | String | Sets the current repeat mode (00-06) |
+| zoom_mode | String | Sets the current zoom mode (00-12) |
+| disc_type | String | The current type of disc in the player (ReadOnly) |
+| audio_type | String | The current audio track type (ReadOnly) |
+| subtitle_type | String | The current subtitle selected (ReadOnly) |
+| aspect_ratio | String | The aspect ratio of the current video output [UDP-203/205 only] (ReadOnly) |
+| source_resolution | String | The video resolution of the content being played (ReadOnly) |
+| output_resolution | String | The video resolution of the player output (ReadOnly) |
+| 3d_indicator | String | Indicates if the content playing is 2D or 3D (ReadOnly) |
+| osd_position | Number | Sets the OSD position (0 to 5) [10x models and up] |
+| sub_shift | Number | Sets the subtitle shift (-10 to 10) [10x models and up] (note more than 5 from 0 throws an error on the BDP103)|
+| hdmi_mode | String | Sets the current HDMI output mode (options vary by model; see notes above for allowed values) |
+| hdr_mode | String | Sets current HDR output mode (Auto, On, Off) [UDP-203/205 only] |
+| remote_button | String | Simulate pressing a button on the remote control (3 letter code; codes can be found in the API documentation) |
+
+## Full Example
+
+oppo.things:
+
+```java
+// direct IP connection
+oppo:player:myoppo "Oppo Blu-ray" [ host="192.168.0.10", model=103, verboseMode=false]
+
+// direct serial connection
+oppo:player:myoppo "Oppo Blu-ray" [ serialPort="COM5", model=103, verboseMode=true]
+
+// serial over IP connection
+oppo:player:myoppo "Oppo Blu-ray" [ host="192.168.0.9", port=4444, model=103, verboseMode=true]
+```
+
+oppo.items:
+
+```java
+Switch oppo_power "Power" { channel="oppo:player:myoppo:power" }
+Dimmer oppo_volume "Volume [%d %%]" { channel="oppo:player:myoppo:volume" }
+Switch oppo_mute "Mute" { channel="oppo:player:myoppo:mute" }
+Number oppo_source "Source Input [%s]" { channel="oppo:player:myoppo:source" }
+String oppo_play_mode "Play Mode [%s]" { channel="oppo:player:myoppo:play_mode" }
+Player oppo_control "Control" { channel="oppo:player:myoppo:control" }
+String oppo_time_mode "Time Mode [%s]" { channel="oppo:player:myoppo:time_mode" }
+Number:Time oppo_time_display "Time [JS(secondsformat.js):%s]" { channel="oppo:player:myoppo:time_display" }
+Number oppo_current_title "Current Title/Track [%s]" { channel="oppo:player:myoppo:current_title" }
+Number oppo_total_title "Total Title/Track [%s]" { channel="oppo:player:myoppo:total_title" }
+Number oppo_current_chapter "Current Chapter [%s]" { channel="oppo:player:myoppo:current_chapter" }
+Number oppo_total_chapter "Total Chapter [%s]" { channel="oppo:player:myoppo:total_chapter" }
+String oppo_repeat_mode "Repeat Mode [%s]" { channel="oppo:player:myoppo:repeat_mode" }
+String oppo_zoom_mode "Zoom Mode [%s]" { channel="oppo:player:myoppo:zoom_mode" }
+String oppo_disc_type "Disc Type [%s]" { channel="oppo:player:myoppo:disc_type" }
+String oppo_audio_type "Audio Type [%s]" { channel="oppo:player:myoppo:audio_type" }
+String oppo_subtitle_type "Subtitle Type [%s]" { channel="oppo:player:myoppo:subtitle_type" }
+String oppo_aspect_ratio "Aspect Ratio [%s]" { channel="oppo:player:myoppo:aspect_ratio" }
+String oppo_source_resolution "Source Resolution [%s]" { channel="oppo:player:myoppo:source_resolution" }
+String oppo_output_resolution "Output Resolution [%s]" { channel="oppo:player:myoppo:output_resolution" }
+String oppo_3d_indicator "3D/2D Indicator [%s]" { channel="oppo:player:myoppo:3d_indicator" }
+Number oppo_osd_position "OSD Position [%s]" { channel="oppo:player:myoppo:osd_position" }
+Number oppo_sub_shift "Subtitle Shift [%s]" { channel="oppo:player:myoppo:sub_shift" }
+String oppo_hdmi_mode "HDMI Mode [%s]" { channel="oppo:player:myoppo:hdmi_mode" }
+String oppo_hdr_mode "HDR Mode [%s]" { channel="oppo:player:myoppo:hdr_mode" }
+String oppo_remote_button "Remote Button [%s]" { channel="oppo:player:myoppo:remote_button" }
+```
+
+secondsformat.js:
+
+```java
+(function(totalSeconds) {
+ if (isNaN(totalSeconds)) {
+ return '-';
+ } else {
+ hours = Math.floor(totalSeconds / 3600);
+ totalSeconds %= 3600;
+ minutes = Math.floor(totalSeconds / 60);
+ seconds = totalSeconds % 60;
+ if ( hours < 10 ) {
+ hours = '0' + hours;
+ }
+ if ( minutes < 10 ) {
+ minutes = '0' + minutes;
+ }
+ if ( seconds < 10 ) {
+ seconds = '0' + seconds;
+ }
+ return hours + ':' + minutes + ':' + seconds;
+ }
+})(input)
+```
+
+oppo.sitemap:
+
+```perl
+sitemap oppo label="Oppo Blu-ray" {
+ Frame label="Player" {
+ Switch item=oppo_power
+ //Volume can be a Setpoint also
+ Slider item=oppo_volume minValue=0 maxValue=100 step=1 visibility=[oppo_power==ON] icon="soundvolume"
+ Switch item=oppo_mute visibility=[oppo_power==ON] icon="soundvolume_mute"
+ Selection item=oppo_source visibility=[oppo_power==ON] icon="player"
+ Text item=oppo_play_mode visibility=[oppo_power==ON] icon="zoom"
+ Default item=oppo_control visibility=[oppo_power==ON]
+ Selection item=oppo_time_mode visibility=[oppo_power==ON] icon="time"
+ Text item=oppo_time_display visibility=[oppo_power==ON] icon="time"
+ Text item=oppo_current_title visibility=[oppo_power==ON] icon="zoom"
+ Text item=oppo_total_title visibility=[oppo_power==ON] icon="zoom"
+ Text item=oppo_current_chapter visibility=[oppo_power==ON] icon="zoom"
+ Text item=oppo_total_chapter visibility=[oppo_power==ON] icon="zoom"
+ Selection item=oppo_repeat_mode visibility=[oppo_power==ON] icon="none"
+ Selection item=oppo_zoom_mode visibility=[oppo_power==ON] icon="none"
+ Text item=oppo_disc_type visibility=[oppo_power==ON] icon="none"
+ Text item=oppo_audio_type visibility=[oppo_power==ON] icon="none"
+ Text item=oppo_subtitle_type visibility=[oppo_power==ON] icon="none"
+ Text item=oppo_aspect_ratio visibility=[oppo_power==ON] icon="none"
+ Text item=oppo_source_resolution visibility=[oppo_power==ON] icon="video"
+ Text item=oppo_output_resolution visibility=[oppo_power==ON] icon="video"
+ Text item=oppo_3d_indicator visibility=[oppo_power==ON] icon="none"
+ Setpoint item=oppo_osd_position label="OSD Position [%d]" minValue=0 maxValue=5 step=1 visibility=[oppo_power==ON]
+ Setpoint item=oppo_sub_shift label="Sub Title Shift [%d]" minValue=-10 maxValue=10 step=1 visibility=[oppo_power==ON]
+ Selection item=oppo_hdmi_mode visibility=[oppo_power==ON] icon="video"
+ Selection item=oppo_hdr_mode visibility=[oppo_power==ON] icon="colorwheel"
+ Selection item=oppo_remote_button visibility=[oppo_power==ON]
+ }
+}
+```
diff --git a/bundles/org.openhab.binding.oppo/pom.xml b/bundles/org.openhab.binding.oppo/pom.xml
new file mode 100644
index 0000000000000..bcab1a8bbb9d9
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 2.5.8-SNAPSHOT
+
+
+ org.openhab.binding.oppo
+
+ openHAB Add-ons :: Bundles :: Oppo Binding
+
+
diff --git a/bundles/org.openhab.binding.oppo/src/main/feature/feature.xml b/bundles/org.openhab.binding.oppo/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..9ebc366e375e2
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/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.oppo/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoBindingConstants.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoBindingConstants.java
new file mode 100644
index 0000000000000..4a88f9b268816
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoBindingConstants.java
@@ -0,0 +1,133 @@
+/**
+ * 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.oppo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+
+/**
+ * The {@link OppoBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class OppoBindingConstants {
+ public static final String BINDING_ID = "oppo";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, "player");
+
+ public static final int MODEL83 = 83;
+ public static final int MODEL103 = 103;
+ public static final int MODEL105 = 105;
+ public static final int MODEL203 = 203;
+ public static final int MODEL205 = 205;
+
+ public static final Integer BDP83_PORT = 19999;
+ public static final Integer BDP10X_PORT = 48360;
+ public static final Integer BDP20X_PORT = 23;
+
+ // List of all Channels
+ public static final String CHANNEL_POWER = "power";
+ public static final String CHANNEL_VOLUME = "volume";
+ public static final String CHANNEL_MUTE = "mute";
+ public static final String CHANNEL_SOURCE = "source";
+ public static final String CHANNEL_PLAY_MODE = "play_mode";
+ public static final String CHANNEL_CONTROL = "control";
+ public static final String CHANNEL_TIME_MODE = "time_mode";
+ public static final String CHANNEL_TIME_DISPLAY = "time_display";
+ public static final String CHANNEL_CURRENT_TITLE = "current_title";
+ public static final String CHANNEL_TOTAL_TITLE = "total_title";
+ public static final String CHANNEL_CURRENT_CHAPTER = "current_chapter";
+ public static final String CHANNEL_TOTAL_CHAPTER = "total_chapter";
+ public static final String CHANNEL_REPEAT_MODE = "repeat_mode";
+ public static final String CHANNEL_ZOOM_MODE = "zoom_mode";
+ public static final String CHANNEL_DISC_TYPE = "disc_type";
+ public static final String CHANNEL_AUDIO_TYPE = "audio_type";
+ public static final String CHANNEL_SUBTITLE_TYPE = "subtitle_type";
+ public static final String CHANNEL_ASPECT_RATIO = "aspect_ratio"; // 203 and 205 only
+ public static final String CHANNEL_SOURCE_RESOLUTION = "source_resolution";
+ public static final String CHANNEL_OUTPUT_RESOLUTION = "output_resolution";
+ public static final String CHANNEL_3D_INDICATOR = "3d_indicator";
+ public static final String CHANNEL_SUB_SHIFT = "sub_shift"; // not on 83
+ public static final String CHANNEL_OSD_POSITION = "osd_position"; // not on 83
+ public static final String CHANNEL_HDMI_MODE = "hdmi_mode";
+ public static final String CHANNEL_HDR_MODE = "hdr_mode"; // 203 and 205 only
+ public static final String CHANNEL_REMOTE_BUTTON = "remote_button";
+
+ // misc
+ public static final String BLANK = "";
+ public static final String SPACE = " ";
+ public static final String SLASH = "/";
+ public static final String UNDERSCORE = "_";
+ public static final String COLON = ":";
+ public static final String ON = "ON";
+ public static final String OFF = "OFF";
+ public static final String ONE = "1";
+ public static final String ZERO = "0";
+ public static final String UNDEF = "UNDEF";
+ public static final String VERBOSE_2 = "2";
+ public static final String VERBOSE_3 = "3";
+ public static final String MUTE = "MUTE";
+ public static final String MUT = "MUT";
+ public static final String UMT = "UMT";
+ public static final String CDDA = "CDDA";
+
+ public static final String NOP = "NOP";
+ public static final String UTC = "UTC";
+ public static final String QTE = "QTE";
+ public static final String QTR = "QTR";
+ public static final String QCE = "QCE";
+ public static final String QCR = "QCR";
+ public static final String QVR = "QVR";
+ public static final String QPW = "QPW";
+ public static final String UPW = "UPW";
+ public static final String QVL = "QVL";
+ public static final String UVL = "UVL";
+ public static final String VUP = "VUP";
+ public static final String VDN = "VDN";
+ public static final String QIS = "QIS";
+ public static final String UIS = "UIS";
+ public static final String UPL = "UPL";
+ public static final String QTK = "QTK";
+ public static final String QCH = "QCH";
+ public static final String QPL = "QPL";
+ public static final String QRP = "QRP";
+ public static final String QZM = "QZM";
+ public static final String UDT = "UDT";
+ public static final String QDT = "QDT";
+ public static final String UAT = "UAT";
+ public static final String QAT = "QAT";
+ public static final String UST = "UST";
+ public static final String QST = "QST";
+ public static final String UAR = "UAR";
+ public static final String UVO = "UVO";
+ public static final String U3D = "U3D";
+ public static final String QSH = "QSH";
+ public static final String QOP = "QOP";
+ public static final String QHD = "QHD";
+ public static final String QHR = "QHR";
+
+ public static final String NO_DISC = "NO DISC";
+ public static final String LOADING = "LOADING";
+ public static final String OPEN = "OPEN";
+ public static final String CLOSE = "CLOSE";
+ public static final String STOP = "STOP";
+ public static final String PLAY = "PLAY";
+
+ public static final String T = "T";
+ public static final String X = "X";
+ public static final String C = "C";
+ public static final String K = "K";
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoException.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoException.java
new file mode 100644
index 0000000000000..b4dd0111dd5e3
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoException.java
@@ -0,0 +1,36 @@
+/**
+ * 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.oppo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link OppoException} class is used for any exception thrown by the binding
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class OppoException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public OppoException() {
+ }
+
+ public OppoException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ public OppoException(String message) {
+ super(message);
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoHandlerFactory.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoHandlerFactory.java
new file mode 100644
index 0000000000000..ff2f6c6eb2969
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoHandlerFactory.java
@@ -0,0 +1,71 @@
+/**
+ * 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.oppo.internal;
+
+import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.eclipse.smarthome.io.transport.serial.SerialPortManager;
+import org.openhab.binding.oppo.internal.handler.OppoHandler;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link OppoHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.oppo", service = ThingHandlerFactory.class)
+public class OppoHandlerFactory extends BaseThingHandlerFactory {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_PLAYER);
+
+ private final SerialPortManager serialPortManager;
+
+ private final OppoStateDescriptionOptionProvider stateDescriptionProvider;
+
+ @Activate
+ public OppoHandlerFactory(final @Reference OppoStateDescriptionOptionProvider provider,
+ final @Reference SerialPortManager serialPortManager) {
+ this.stateDescriptionProvider = provider;
+ this.serialPortManager = serialPortManager;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
+ OppoHandler handler = new OppoHandler(thing, stateDescriptionProvider, serialPortManager);
+
+ return handler;
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoStateDescriptionOptionProvider.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoStateDescriptionOptionProvider.java
new file mode 100644
index 0000000000000..e4537e3dcb372
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/OppoStateDescriptionOptionProvider.java
@@ -0,0 +1,41 @@
+/**
+ * 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.oppo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.eclipse.smarthome.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Dynamic provider of state options while leaving other state description fields as original.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, OppoStateDescriptionOptionProvider.class })
+@NonNullByDefault
+public class OppoStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
+
+ @Reference
+ protected void setChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+ }
+
+ protected void unsetChannelTypeI18nLocalizationService(
+ final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+ this.channelTypeI18nLocalizationService = null;
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoCommand.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoCommand.java
new file mode 100644
index 0000000000000..eb1ceeeb28e65
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoCommand.java
@@ -0,0 +1,92 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Represents the different kinds of commands
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public enum OppoCommand {
+ POWER_ON("PON"),
+ POWER_OFF("POF"),
+ PLAY("PLA"),
+ PAUSE("PAU"),
+ PREV("PRE"),
+ REWIND("REV"),
+ FFORWARD("FWD"),
+ NEXT("NXT"),
+ MUTE("MUT"),
+ QUERY_POWER_STATUS("QPW"),
+ QUERY_FIRMWARE_VERSION("QVR"),
+ QUERY_VOLUME("QVL"),
+ QUERY_HDMI_RESOLUTION("QHD"),
+ QUERY_HDR_SETTING("QHR"),
+ QUERY_PLAYBACK_STATUS("QPL"),
+ QUERY_TITLE_TRACK("QTK"),
+ QUERY_CHAPTER("QCH"),
+ QUERY_TITLE_ELAPSED("QTE"),
+ QUERY_TITLE_REMAIN("QTR"),
+ QUERY_CHAPTER_ELAPSED("QCE"),
+ QUERY_CHAPTER_REMAIN("QCR"),
+ QUERY_DISC_TYPE("QDT"),
+ QUERY_AUDIO_TYPE("QAT"),
+ QUERY_SUBTITLE_TYPE("QST"),
+ QUERY_SUBTITLE_SHIFT("QSH"),
+ QUERY_OSD_POSITION("QOP"),
+ QUERY_REPEAT_MODE("QRP"),
+ QUERY_ZOOM_MODE("QZM"),
+ QUERY_INPUT_SOURCE("QIS"),
+ SET_VERBOSE_MODE("SVM"),
+ SET_HDMI_MODE("SHD"),
+ SET_HDR_MODE("SHR"),
+ SET_ZOOM_RATIO("SZM"),
+ SET_VOLUME_LEVEL("SVL"),
+ SET_REPEAT("SRP"),
+ SET_SUBTITLE_SHIFT("SSH"),
+ SET_OSD_POSITION("SOP"),
+ SET_TIME_DISPLAY("STC"),
+ SET_INPUT_SOURCE("SIS"),
+ NO_OP("NOP");
+
+ private String value;
+
+ public static final Set INITIAL_COMMANDS = new HashSet<>(
+ Arrays.asList(QUERY_POWER_STATUS, QUERY_FIRMWARE_VERSION, QUERY_VOLUME, QUERY_HDMI_RESOLUTION,
+ QUERY_HDR_SETTING, QUERY_PLAYBACK_STATUS, QUERY_DISC_TYPE, QUERY_AUDIO_TYPE, QUERY_SUBTITLE_SHIFT,
+ QUERY_OSD_POSITION, QUERY_REPEAT_MODE, QUERY_ZOOM_MODE, QUERY_INPUT_SOURCE));
+
+ public static final Set QUERY_COMMANDS = new HashSet<>(
+ Arrays.asList(QUERY_VOLUME, QUERY_HDMI_RESOLUTION, QUERY_PLAYBACK_STATUS, QUERY_DISC_TYPE, QUERY_AUDIO_TYPE,
+ QUERY_SUBTITLE_SHIFT, QUERY_OSD_POSITION, QUERY_REPEAT_MODE, QUERY_ZOOM_MODE, QUERY_INPUT_SOURCE));
+
+ OppoCommand(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Get the command name
+ *
+ * @return the command name
+ */
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java
new file mode 100644
index 0000000000000..d6ab876362a0b
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoConnector.java
@@ -0,0 +1,281 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.oppo.internal.OppoException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class for communicating with the Oppo player
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Oppo binding
+ */
+@NonNullByDefault
+public abstract class OppoConnector {
+ private static final Pattern QRY_PATTERN = Pattern.compile("^@(Q[A-Z0-9]{2}|VUP|VDN) OK (.*)$");
+ private static final Pattern STUS_PATTERN = Pattern.compile("^@(U[A-Z0-9]{2}) (.*)$");
+
+ private static final String NOP_OK = "@NOP OK";
+ private static final String NOP = "NOP";
+ private static final String OK = "OK";
+
+ private final Logger logger = LoggerFactory.getLogger(OppoConnector.class);
+
+ private String beginCmd = "#";
+ private String endCmd = "\r";
+
+ /** The output stream */
+ protected @Nullable OutputStream dataOut;
+
+ /** The input stream */
+ protected @Nullable InputStream dataIn;
+
+ /** true if the connection is established, false if not */
+ private boolean connected;
+
+ private @Nullable Thread readerThread;
+
+ private List listeners = new ArrayList<>();
+
+ /**
+ * Called when using direct IP connection for 83/93/95/103/105
+ * overrides the command message preamble and removes the CR at the end
+ */
+ public void overrideCmdPreamble(boolean override) {
+ if (override) {
+ this.beginCmd = "REMOTE ";
+ this.endCmd = "";
+ }
+ }
+
+ /**
+ * Get whether the connection is established or not
+ *
+ * @return true if the connection is established
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Set whether the connection is established or not
+ *
+ * @param connected true if the connection is established
+ */
+ protected void setConnected(boolean connected) {
+ this.connected = connected;
+ }
+
+ /**
+ * Set the thread that handles the feedback messages
+ *
+ * @param readerThread the thread
+ */
+ protected void setReaderThread(Thread readerThread) {
+ this.readerThread = readerThread;
+ }
+
+ /**
+ * Open the connection with the Oppo player
+ *
+ * @throws OppoException - In case of any problem
+ */
+ public abstract void open() throws OppoException;
+
+ /**
+ * Close the connection with the Oppo player
+ */
+ public abstract void close();
+
+ /**
+ * Stop the thread that handles the feedback messages and close the opened input and output streams
+ */
+ protected void cleanup() {
+ Thread readerThread = this.readerThread;
+ OutputStream dataOut = this.dataOut;
+ if (dataOut != null) {
+ try {
+ dataOut.close();
+ } catch (IOException e) {
+ logger.debug("Error closing dataOut: {}", e.getMessage());
+ }
+ this.dataOut = null;
+ }
+ InputStream dataIn = this.dataIn;
+ if (dataIn != null) {
+ try {
+ dataIn.close();
+ } catch (IOException e) {
+ logger.debug("Error closing dataIn: {}", e.getMessage());
+ }
+ this.dataIn = null;
+ }
+ if (readerThread != null) {
+ readerThread.interrupt();
+ try {
+ readerThread.join(3000);
+ } catch (InterruptedException e) {
+ logger.warn("Error joining readerThread: {}", e.getMessage());
+ }
+ this.readerThread = null;
+ }
+ }
+
+ /**
+ * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
+ * actually read is returned as an integer.
+ *
+ * @param dataBuffer the buffer into which the data is read.
+ *
+ * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
+ * stream has been reached.
+ *
+ * @throws OppoException - If the input stream is null, if the first byte cannot be read for any reason
+ * other than the end of the file, if the input stream has been closed, or if some other I/O error
+ * occurs.
+ */
+ protected int readInput(byte[] dataBuffer) throws OppoException {
+ InputStream dataIn = this.dataIn;
+ if (dataIn == null) {
+ throw new OppoException("readInput failed: input stream is null");
+ }
+ try {
+ return dataIn.read(dataBuffer);
+ } catch (IOException e) {
+ throw new OppoException("readInput failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Request the Oppo controller to execute a command and pass in a value
+ *
+ * @param cmd the command to execute
+ * @param value the string value to pass with the command
+ *
+ * @throws OppoException - In case of any problem
+ */
+ public void sendCommand(OppoCommand cmd, @Nullable String value) throws OppoException {
+ sendCommand(cmd.getValue() + " " + value);
+ }
+
+ /**
+ * Request the Oppo controller to execute a command that does not specify a value
+ *
+ * @param cmd the command to execute
+ *
+ * @throws OppoException - In case of any problem
+ */
+ public void sendCommand(OppoCommand cmd) throws OppoException {
+ sendCommand(cmd.getValue());
+ }
+
+ /**
+ * Request the Oppo controller to execute a raw command string
+ *
+ * @param command the command string to run
+ *
+ * @throws OppoException - In case of any problem
+ */
+ public void sendCommand(String command) throws OppoException {
+ String messageStr = beginCmd + command + endCmd;
+ logger.debug("Sending command: {}", messageStr);
+
+ OutputStream dataOut = this.dataOut;
+ if (dataOut == null) {
+ throw new OppoException("Send command \"" + messageStr + "\" failed: output stream is null");
+ }
+ try {
+ dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
+ dataOut.flush();
+ } catch (IOException e) {
+ logger.debug("Send command \"{}\" failed: {}", messageStr, e.getMessage());
+ throw new OppoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Add a listener to the list of listeners to be notified with events
+ *
+ * @param listener the listener
+ */
+ public void addEventListener(OppoMessageEventListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Remove a listener from the list of listeners to be notified with events
+ *
+ * @param listener the listener
+ */
+ public void removeEventListener(OppoMessageEventListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
+ *
+ * @param incomingMessage the received message
+ */
+ public void handleIncomingMessage(byte[] incomingMessage) {
+ String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
+
+ logger.debug("handleIncomingMessage: {}", message);
+
+ if (NOP_OK.equals(message)) {
+ dispatchKeyValue(NOP, OK);
+ return;
+ }
+
+ // Player sent an OK response to a query: @QDT OK DVD-VIDEO or a volume update @VUP OK 100
+ Matcher matcher = QRY_PATTERN.matcher(message);
+ if (matcher.find()) {
+ // pull out the inquiry type and the remainder of the message
+ dispatchKeyValue(matcher.group(1), matcher.group(2));
+ return;
+ }
+
+ // Player sent a status update ie: @UTC 000 000 T 00:00:01
+ matcher = STUS_PATTERN.matcher(message);
+ if (matcher.find()) {
+ // pull out the update type and the remainder of the message
+ dispatchKeyValue(matcher.group(1), matcher.group(2));
+ return;
+ }
+
+ logger.debug("unhandled message: {}", message);
+ }
+
+ /**
+ * Dispatch an event (key, value) to the event listeners
+ *
+ * @param key the key
+ * @param value the value
+ */
+ private void dispatchKeyValue(String key, String value) {
+ OppoMessageEvent event = new OppoMessageEvent(this, key, value);
+ listeners.forEach(l -> l.onNewMessageEvent(event));
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoDefaultConnector.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoDefaultConnector.java
new file mode 100644
index 0000000000000..ab9aa488f7ed9
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoDefaultConnector.java
@@ -0,0 +1,40 @@
+/**
+ * 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.oppo.internal.communication;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.oppo.internal.OppoException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class to create a default MonopriceAudioConnector before initialization is complete.
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Oppo binding
+ */
+@NonNullByDefault
+public class OppoDefaultConnector extends OppoConnector {
+ private final Logger logger = LoggerFactory.getLogger(OppoDefaultConnector.class);
+
+ @Override
+ public void open() throws OppoException {
+ logger.warn("Oppo binding incorrectly configured. Please configure for Serial or IP connection");
+ setConnected(false);
+ }
+
+ @Override
+ public void close() {
+ setConnected(false);
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoIpConnector.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoIpConnector.java
new file mode 100644
index 0000000000000..aaf149b48e244
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoIpConnector.java
@@ -0,0 +1,123 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.oppo.internal.OppoException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for communicating with the Oppo player directly or through a serial over IP connection
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Oppo binding
+ */
+@NonNullByDefault
+public class OppoIpConnector extends OppoConnector {
+ private final Logger logger = LoggerFactory.getLogger(OppoIpConnector.class);
+
+ private @Nullable String address;
+ private int port;
+
+ private @Nullable Socket clientSocket;
+
+ /**
+ * Constructor
+ *
+ * @param address the IP address of the player or serial over ip adapter
+ * @param port the TCP port to be used
+ */
+ public OppoIpConnector(@Nullable String address, int port) {
+ this.address = address;
+ this.port = port;
+ }
+
+ @Override
+ public synchronized void open() throws OppoException {
+ logger.debug("Opening IP connection on IP {} port {}", this.address, this.port);
+ try {
+ Socket clientSocket = new Socket(this.address, this.port);
+ clientSocket.setSoTimeout(100);
+
+ dataOut = new DataOutputStream(clientSocket.getOutputStream());
+ dataIn = new DataInputStream(clientSocket.getInputStream());
+
+ Thread thread = new OppoReaderThread(this, this.address + ":" + this.port);
+ setReaderThread(thread);
+ thread.start();
+
+ this.clientSocket = clientSocket;
+
+ setConnected(true);
+
+ logger.debug("IP connection opened");
+ } catch (IOException | SecurityException | IllegalArgumentException e) {
+ setConnected(false);
+ throw new OppoException("Opening IP connection failed: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ logger.debug("Closing IP connection");
+ super.cleanup();
+ Socket clientSocket = this.clientSocket;
+ if (clientSocket != null) {
+ try {
+ clientSocket.close();
+ } catch (IOException e) {
+ }
+ this.clientSocket = null;
+ }
+ setConnected(false);
+ logger.debug("IP connection closed");
+ }
+
+ /**
+ * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
+ * actually read is returned as an integer.
+ * In case of socket timeout, the returned value is 0.
+ *
+ * @param dataBuffer the buffer into which the data is read.
+ *
+ * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
+ * stream has been reached.
+ *
+ * @throws OppoException - If the input stream is null, if the first byte cannot be read for any reason
+ * other than the end of the file, if the input stream has been closed, or if some other I/O error
+ * occurs.
+ */
+ @Override
+ protected int readInput(byte[] dataBuffer) throws OppoException {
+ InputStream dataIn = this.dataIn;
+ if (dataIn == null) {
+ throw new OppoException("readInput failed: input stream is null");
+ }
+ try {
+ return dataIn.read(dataBuffer);
+ } catch (SocketTimeoutException e) {
+ return 0;
+ } catch (IOException e) {
+ throw new OppoException("readInput failed: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoMessageEvent.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoMessageEvent.java
new file mode 100644
index 0000000000000..5a32eab728165
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoMessageEvent.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.oppo.internal.communication;
+
+import java.util.EventObject;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * OppoMessageEvent event used to notify changes coming from messages received from the Oppo player
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class OppoMessageEvent extends EventObject {
+
+ private static final long serialVersionUID = 1L;
+ private String key;
+ private String value;
+
+ public OppoMessageEvent(Object source, String key, String value) {
+ super(source);
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoMessageEventListener.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoMessageEventListener.java
new file mode 100644
index 0000000000000..3c3a124ec66a0
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoMessageEventListener.java
@@ -0,0 +1,33 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.util.EventListener;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Oppo Event Listener interface. Handles incoming Oppo message events
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public interface OppoMessageEventListener extends EventListener {
+
+ /**
+ * Event handler method for incoming Oppo message events
+ *
+ * @param event the OppoMessageEvent object
+ */
+ public void onNewMessageEvent(OppoMessageEvent event);
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoReaderThread.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoReaderThread.java
new file mode 100644
index 0000000000000..a4b9223464182
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoReaderThread.java
@@ -0,0 +1,83 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.util.Arrays;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.oppo.internal.OppoBindingConstants;
+import org.openhab.binding.oppo.internal.OppoException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A class that reads messages from the Oppo player in a dedicated thread
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Oppo binding
+ */
+@NonNullByDefault
+public class OppoReaderThread extends Thread {
+ private static final int READ_BUFFER_SIZE = 16;
+ private static final int SIZE = 64;
+ private static final char TERM_CHAR = '\r';
+
+ private final Logger logger = LoggerFactory.getLogger(OppoReaderThread.class);
+
+ private OppoConnector connector;
+
+ /**
+ * Constructor
+ *
+ * @param connector the object that should handle the received message
+ */
+ public OppoReaderThread(OppoConnector connector, String uid) {
+ super(OppoBindingConstants.BINDING_ID + "-" + uid);
+ this.connector = connector;
+ setDaemon(true);
+ }
+
+ @Override
+ public void run() {
+ logger.debug("Data listener started");
+
+ byte[] readDataBuffer = new byte[READ_BUFFER_SIZE];
+ byte[] dataBuffer = new byte[SIZE];
+ int index = 0;
+
+ try {
+ while (!Thread.interrupted()) {
+ int len = connector.readInput(readDataBuffer);
+ if (len > 0) {
+ for (int i = 0; i < len; i++) {
+ if (index < SIZE) {
+ dataBuffer[index++] = readDataBuffer[i];
+ }
+ if (readDataBuffer[i] == TERM_CHAR) {
+ if (index >= SIZE) {
+ dataBuffer[index - 1] = (byte) TERM_CHAR;
+ }
+ byte[] msg = Arrays.copyOf(dataBuffer, index);
+ connector.handleIncomingMessage(msg);
+ index = 0;
+ }
+ }
+ }
+ }
+ } catch (OppoException e) {
+ logger.debug("Reading failed: {}", e.getMessage(), e);
+ }
+
+ logger.debug("Data listener stopped");
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoSerialConnector.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoSerialConnector.java
new file mode 100644
index 0000000000000..a031d4d2542a5
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoSerialConnector.java
@@ -0,0 +1,124 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.io.transport.serial.PortInUseException;
+import org.eclipse.smarthome.io.transport.serial.SerialPort;
+import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier;
+import org.eclipse.smarthome.io.transport.serial.SerialPortManager;
+import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException;
+import org.openhab.binding.oppo.internal.OppoException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class for communicating with the Oppo player through a serial connection
+ *
+ * @author Laurent Garnier - Initial contribution
+ * @author Michael Lobstein - Adapted for the Oppo binding
+ */
+@NonNullByDefault
+public class OppoSerialConnector extends OppoConnector {
+ private final Logger logger = LoggerFactory.getLogger(OppoSerialConnector.class);
+
+ private String serialPortName;
+ private SerialPortManager serialPortManager;
+
+ private @Nullable SerialPort serialPort;
+
+ /**
+ * Constructor
+ *
+ * @param serialPortManager the serial port manager
+ * @param serialPortName the serial port name to be used
+ */
+ public OppoSerialConnector(SerialPortManager serialPortManager, String serialPortName) {
+ this.serialPortManager = serialPortManager;
+ this.serialPortName = serialPortName;
+ }
+
+ @Override
+ public synchronized void open() throws OppoException {
+ logger.debug("Opening serial connection on port {}", serialPortName);
+ try {
+ SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
+ if (portIdentifier == null) {
+ setConnected(false);
+ throw new OppoException("Opening serial connection failed: No Such Port");
+ }
+
+ SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
+
+ commPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
+ commPort.enableReceiveThreshold(1);
+ commPort.enableReceiveTimeout(100);
+ commPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
+
+ InputStream dataIn = commPort.getInputStream();
+ OutputStream dataOut = commPort.getOutputStream();
+
+ if (dataOut != null) {
+ dataOut.flush();
+ }
+ if (dataIn != null && dataIn.markSupported()) {
+ try {
+ dataIn.reset();
+ } catch (IOException e) {
+ }
+ }
+
+ Thread thread = new OppoReaderThread(this, this.serialPortName);
+ setReaderThread(thread);
+ thread.start();
+
+ this.serialPort = commPort;
+ this.dataIn = dataIn;
+ this.dataOut = dataOut;
+
+ setConnected(true);
+
+ logger.debug("Serial connection opened");
+ } catch (PortInUseException e) {
+ setConnected(false);
+ throw new OppoException("Opening serial connection failed: Port in Use Exception", e);
+ } catch (UnsupportedCommOperationException e) {
+ setConnected(false);
+ throw new OppoException("Opening serial connection failed: Unsupported Comm Operation Exception", e);
+ } catch (IOException e) {
+ setConnected(false);
+ throw new OppoException("Opening serial connection failed: IO Exception", e);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ logger.debug("Closing serial connection");
+ SerialPort serialPort = this.serialPort;
+ if (serialPort != null) {
+ serialPort.removeEventListener();
+ }
+ super.cleanup();
+ if (serialPort != null) {
+ serialPort.close();
+ this.serialPort = null;
+ }
+ setConnected(false);
+ logger.debug("Serial connection closed");
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoStatusCodes.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoStatusCodes.java
new file mode 100644
index 0000000000000..891f8cd190c87
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/communication/OppoStatusCodes.java
@@ -0,0 +1,57 @@
+/**
+ * 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.oppo.internal.communication;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Provides mapping of various Oppo query status codes to the corresponding set codes
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+
+@NonNullByDefault
+public class OppoStatusCodes {
+ // map to lookup random mode
+ public static final Map REPEAT_MODE = new HashMap<>();
+ static {
+ REPEAT_MODE.put("00", "OFF");
+ REPEAT_MODE.put("01", "ONE"); // maybe?"
+ REPEAT_MODE.put("02", "CH");
+ REPEAT_MODE.put("03", "ALL");
+ REPEAT_MODE.put("04", "TT");
+ REPEAT_MODE.put("05", "SHF");
+ REPEAT_MODE.put("06", "RND");
+ }
+
+ // map to lookup zoom mode
+ public static final Map ZOOM_MODE = new HashMap<>();
+ static {
+ ZOOM_MODE.put("00", "1"); // Off (zoom 1x)
+ ZOOM_MODE.put("01", "AR"); // Stretch
+ ZOOM_MODE.put("02", "FS"); // Full screen
+ ZOOM_MODE.put("03", "US"); // Underscan
+ ZOOM_MODE.put("04", "1.2");
+ ZOOM_MODE.put("05", "1.3");
+ ZOOM_MODE.put("06", "1.5");
+ ZOOM_MODE.put("07", "2");
+ ZOOM_MODE.put("08", "3");
+ ZOOM_MODE.put("09", "4");
+ ZOOM_MODE.put("10", "1/2");
+ ZOOM_MODE.put("11", "1/3");
+ ZOOM_MODE.put("12", "1/4");
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/configuration/OppoThingConfiguration.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/configuration/OppoThingConfiguration.java
new file mode 100644
index 0000000000000..b089eb1799e8f
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/configuration/OppoThingConfiguration.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.oppo.internal.configuration;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link OppoThingConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class OppoThingConfiguration {
+ public @Nullable Integer model;
+ public @Nullable String serialPort;
+ public @Nullable String host;
+ public @Nullable Integer port;
+ public boolean verboseMode;
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java
new file mode 100644
index 0000000000000..fbee70d6b9798
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/discovery/OppoDiscoveryService.java
@@ -0,0 +1,293 @@
+/**
+ * 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.oppo.internal.discovery;
+
+import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.eclipse.smarthome.io.net.http.HttpUtil;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovery class for the Oppo Blu-ray Player line.
+ * The player sends SDDP packets continuously for us to discover.
+ *
+ * @author Tim Roberts - Initial contribution
+ * @author Michael Lobstein - Adapted for the Oppo binding
+ */
+
+@NonNullByDefault
+@Component(service = DiscoveryService.class, configurationPid = "discovery.oppo")
+public class OppoDiscoveryService extends AbstractDiscoveryService {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_PLAYER);
+
+ private final Logger logger = LoggerFactory.getLogger(OppoDiscoveryService.class);
+
+ /**
+ * Address SDDP broadcasts on
+ */
+ private static final String SDDP_ADDR = "239.255.255.251";
+
+ /**
+ * Port number SDDP uses
+ */
+ private static final int SDDP_PORT = 7624;
+
+ /**
+ * SDDP packet should be only 512 in size - make it 600 to give us some room
+ */
+ private static final int BUFFER_SIZE = 600;
+
+ /**
+ * Socket read timeout (in ms) - allows us to shutdown the listening every TIMEOUT
+ */
+ private static final int TIMEOUT_MS = 1000;
+
+ /**
+ * Whether we are currently scanning or not
+ */
+ private boolean scanning;
+
+ /**
+ * The {@link ExecutorService} to run the listening threads on.
+ */
+ private @Nullable ExecutorService executorService;
+
+ private static final String DISPLAY_NAME_83 = "OPPO BDP-83/93/95";
+ private static final String DISPLAY_NAME_103 = "OPPO BDP-103";
+ private static final String DISPLAY_NAME_105 = "OPPO BDP-105";
+
+ /**
+ * Constructs the discovery class using the thing IDs that we can discover.
+ */
+ public OppoDiscoveryService() {
+ super(SUPPORTED_THING_TYPES_UIDS, 30, false);
+ }
+
+ @Override
+ public Set getSupportedThingTypes() {
+ return SUPPORTED_THING_TYPES_UIDS;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Starts the scan. This discovery will:
+ *
+ *
Request all the network interfaces
+ *
For each network interface, create a listening thread using {@link #executorService}
+ *
Each listening thread will open up a {@link MulticastSocket} using {@link #SDDP_ADDR} and {@link #SDDP_PORT}
+ * and
+ * will receive any {@link DatagramPacket} that comes in
+ *
The {@link DatagramPacket} is then investigated to see if is a SDDP packet and will create a new thing from
+ * it
+ *
+ * The process will continue until {@link #stopScan()} is called.
+ */
+ @Override
+ protected void startScan() {
+ if (executorService != null) {
+ stopScan();
+ }
+
+ logger.debug("Starting Discovery");
+
+ try {
+ final InetAddress addr = InetAddress.getByName(SDDP_ADDR);
+ final List networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ final ExecutorService service = Executors.newFixedThreadPool(networkInterfaces.size());
+ executorService = service;
+
+ scanning = true;
+ for (final NetworkInterface netint : networkInterfaces) {
+
+ service.execute(() -> {
+ try {
+ MulticastSocket multiSocket = new MulticastSocket(SDDP_PORT);
+ multiSocket.setSoTimeout(TIMEOUT_MS);
+ multiSocket.setNetworkInterface(netint);
+ multiSocket.joinGroup(addr);
+
+ while (scanning) {
+ DatagramPacket receivePacket = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
+ try {
+ multiSocket.receive(receivePacket);
+
+ String message = new String(receivePacket.getData(), StandardCharsets.US_ASCII).trim();
+ if (message != null && message.length() > 0) {
+ messageReceive(message);
+ }
+ } catch (SocketTimeoutException e) {
+ // ignore
+ }
+ }
+
+ multiSocket.close();
+ } catch (IOException e) {
+ if (!e.getMessage().contains("No IP addresses bound to interface")) {
+ logger.debug("OppoDiscoveryService IOException: {}", e.getMessage(), e);
+ }
+ }
+ });
+ }
+ } catch (IOException e) {
+ logger.debug("OppoDiscoveryService IOException: {}", e.getMessage(), e);
+ }
+ }
+
+ /**
+ * SDDP message has the following format
+ *
+ *
+ * Notify: OPPO Player Start
+ * Server IP: 192.168.0.2
+ * Server Port: 23
+ * Server Name: OPPO UDP-203
+ *
+ *
+ *
+ * @param message possibly null, possibly empty SDDP message
+ */
+ private void messageReceive(String message) {
+ if (message.trim().length() == 0) {
+ return;
+ }
+
+ String host = null;
+ String port = null;
+ Integer model = null;
+ String displayName = null;
+
+ for (String msg : message.split("\n")) {
+ String[] line = msg.split(":");
+
+ if (line.length == 2) {
+ if (line[0].contains("Server IP")) {
+ host = line[1].trim();
+ }
+
+ if (line[0].contains("Server Port")) {
+ port = line[1].trim();
+ }
+
+ if (line[0].contains("Server Name")) {
+ // example: "OPPO UDP-203"
+ // note: Server Name only provided on UDP models, not present on BDP models
+ displayName = line[1].trim();
+ }
+ } else {
+ logger.debug("messageReceive() - Unable to process line: {}", msg);
+ }
+ }
+
+ // by looking at the port number we can mostly determine what the model number is
+ if (host != null && port != null) {
+ if (BDP83_PORT.toString().equals(port)) {
+ model = MODEL83;
+ displayName = DISPLAY_NAME_83;
+ } else if (BDP10X_PORT.toString().equals(port)) {
+ // The older models do not have the "Server Name" in the discovery packet
+ // for the 10x we need to get the DLNA service list page and find modelNumber there
+ // in order to determine if this is a BDP-103 or BDP-105
+ try {
+ String result = HttpUtil.executeUrl("GET", "http://" + host + ":2870/dmr.xml", 5000);
+
+ if (result != null && result.contains("OPPO BDP-103")) {
+ model = MODEL103;
+ displayName = DISPLAY_NAME_103;
+ } else if (result != null && result.contains("OPPO BDP-105")) {
+ model = MODEL105;
+ displayName = DISPLAY_NAME_105;
+ } else {
+ model = MODEL103;
+ displayName = DISPLAY_NAME_103;
+ }
+ } catch (IOException e) {
+ logger.debug("Error getting player DLNA info page: {}", e.getMessage());
+ // the call failed for some reason, just assume we are a 103
+ model = MODEL103;
+ displayName = DISPLAY_NAME_103;
+ }
+ } else if (BDP20X_PORT.toString().equals(port)) {
+ if (displayName != null && displayName.contains(Integer.toString(MODEL203))) {
+ model = MODEL203;
+ } else if (displayName != null && displayName.contains(Integer.toString(MODEL205))) {
+ model = MODEL205;
+ } else {
+ model = MODEL203;
+ displayName = "Unknown OPPO UDP player";
+ }
+ }
+
+ if (model != null) {
+ ThingUID uid = new ThingUID(THING_TYPE_PLAYER, host.replace(".", "_"));
+ HashMap properties = new HashMap<>();
+ properties.put("model", model);
+ properties.put("host", host);
+
+ DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
+ .withRepresentationProperty("host").withLabel(displayName + " (" + host + ")").build();
+
+ this.thingDiscovered(result);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally
+ * within {@link #TIMEOUT) * 5 time then shutdown the {@link #executorService}
+ */
+ @Override
+ protected synchronized void stopScan() {
+ super.stopScan();
+ ExecutorService service = executorService;
+ if (service == null) {
+ return;
+ }
+
+ scanning = false;
+
+ try {
+ service.awaitTermination(TIMEOUT_MS * 5, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ }
+ service.shutdown();
+ executorService = null;
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java
new file mode 100644
index 0000000000000..46aefa1be1363
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/java/org/openhab/binding/oppo/internal/handler/OppoHandler.java
@@ -0,0 +1,881 @@
+/**
+ * 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.oppo.internal.handler;
+
+import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.NextPreviousType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.PlayPauseType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.types.RewindFastforwardType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.StateOption;
+import org.eclipse.smarthome.core.types.UnDefType;
+import org.eclipse.smarthome.io.transport.serial.SerialPortManager;
+import org.openhab.binding.oppo.internal.OppoException;
+import org.openhab.binding.oppo.internal.OppoStateDescriptionOptionProvider;
+import org.openhab.binding.oppo.internal.communication.OppoCommand;
+import org.openhab.binding.oppo.internal.communication.OppoConnector;
+import org.openhab.binding.oppo.internal.communication.OppoDefaultConnector;
+import org.openhab.binding.oppo.internal.communication.OppoIpConnector;
+import org.openhab.binding.oppo.internal.communication.OppoMessageEvent;
+import org.openhab.binding.oppo.internal.communication.OppoMessageEventListener;
+import org.openhab.binding.oppo.internal.communication.OppoSerialConnector;
+import org.openhab.binding.oppo.internal.communication.OppoStatusCodes;
+import org.openhab.binding.oppo.internal.configuration.OppoThingConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link OppoHandler} is responsible for handling commands, which are sent to one of the channels.
+ *
+ * Based on the Rotel binding by Laurent Garnier
+ *
+ * @author Michael Lobstein - Initial contribution
+ */
+@NonNullByDefault
+public class OppoHandler extends BaseThingHandler implements OppoMessageEventListener {
+ private static final long RECON_POLLING_INTERVAL_SEC = 60;
+ private static final long POLLING_INTERVAL_SEC = 15;
+ private static final long INITIAL_POLLING_DELAY_SEC = 10;
+ private static final long SLEEP_BETWEEN_CMD_MS = 100;
+
+ private static final Pattern TIME_CODE_PATTERN = Pattern
+ .compile("^(\\d{3}) (\\d{3}) ([A-Z]{1}) (\\d{2}:\\d{2}:\\d{2})$");
+
+ private final Logger logger = LoggerFactory.getLogger(OppoHandler.class);
+
+ private @Nullable ScheduledFuture> reconnectJob;
+ private @Nullable ScheduledFuture> pollingJob;
+
+ private OppoStateDescriptionOptionProvider stateDescriptionProvider;
+ private SerialPortManager serialPortManager;
+ private OppoConnector connector = new OppoDefaultConnector();
+
+ private List inputSourceOptions = new ArrayList<>();
+ private List hdmiModeOptions = new ArrayList<>();
+
+ private long lastEventReceived = System.currentTimeMillis();
+ private String versionString = BLANK;
+ private String verboseMode = VERBOSE_2;
+ private String currentChapter = BLANK;
+ private String currentTimeMode = T;
+ private String currentPlayMode = BLANK;
+ private String currentDiscType = BLANK;
+ private boolean isPowerOn = false;
+ private boolean isUDP20X = false;
+ private boolean isBdpIP = false;
+ private Object sequenceLock = new Object();
+
+ /**
+ * Constructor
+ */
+ public OppoHandler(Thing thing, OppoStateDescriptionOptionProvider stateDescriptionProvider,
+ SerialPortManager serialPortManager) {
+ super(thing);
+ this.stateDescriptionProvider = stateDescriptionProvider;
+ this.serialPortManager = serialPortManager;
+ }
+
+ @Override
+ public void initialize() {
+ OppoThingConfiguration config = getConfigAs(OppoThingConfiguration.class);
+
+ // Check configuration settings
+ String configError = null;
+ boolean override = false;
+
+ Integer model = config.model;
+ String serialPort = config.serialPort;
+ String host = config.host;
+ Integer port = config.port;
+
+ if (model == null) {
+ configError = "player model must be specified";
+ return;
+ }
+
+ if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
+ configError = "undefined serialPort and host configuration settings; please set one of them";
+ } else if (serialPort != null && (host == null || host.isEmpty())) {
+ if (serialPort.toLowerCase().startsWith("rfc2217")) {
+ configError = "use host and port configuration settings for a serial over IP connection";
+ }
+ } else {
+ if (port == null) {
+ if (model == MODEL83) {
+ port = BDP83_PORT;
+ override = true;
+ this.isBdpIP = true;
+ } else if (model == MODEL103 || model == MODEL105) {
+ port = BDP10X_PORT;
+ override = true;
+ this.isBdpIP = true;
+ } else {
+ port = BDP20X_PORT;
+ }
+ } else if (port <= 0) {
+ configError = "invalid port configuration setting";
+ }
+ }
+
+ if (configError != null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
+ return;
+ }
+
+ if (serialPort != null) {
+ connector = new OppoSerialConnector(serialPortManager, serialPort);
+ } else if (port != null) {
+ connector = new OppoIpConnector(host, port);
+ connector.overrideCmdPreamble(override);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Either Serial port or Host & Port must be specifed");
+ return;
+ }
+
+ if (config.verboseMode) {
+ this.verboseMode = VERBOSE_3;
+ }
+
+ if (model == MODEL203 || model == MODEL205) {
+ this.isUDP20X = true;
+ }
+
+ this.buildOptionDropdowns(model);
+ stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
+ inputSourceOptions);
+ stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_HDMI_MODE),
+ hdmiModeOptions);
+
+ // remove channels not needed for this model
+ List channels = new ArrayList<>(this.getThing().getChannels());
+
+ if (model == MODEL83) {
+ channels.removeIf(c -> (c.getUID().getId().equals(CHANNEL_SUB_SHIFT)
+ || c.getUID().getId().equals(CHANNEL_OSD_POSITION)));
+ }
+
+ if (model == MODEL83 || model == MODEL103 || model == MODEL105) {
+ channels.removeIf(c -> (c.getUID().getId().equals(CHANNEL_ASPECT_RATIO)
+ || c.getUID().getId().equals(CHANNEL_HDR_MODE)));
+ }
+
+ // no query to determine this, so set the default value at startup
+ updateChannelState(CHANNEL_TIME_MODE, currentTimeMode);
+
+ updateThing(editThing().withChannels(channels).build());
+
+ scheduleReconnectJob();
+ schedulePollingJob();
+
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+
+ @Override
+ public void dispose() {
+ cancelReconnectJob();
+ cancelPollingJob();
+ closeConnection();
+ super.dispose();
+ }
+
+ /**
+ * Handle a command the UI
+ *
+ * @param channelUID the channel sending the command
+ * @param command the command received
+ *
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String channel = channelUID.getId();
+
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
+ return;
+ }
+
+ if (!connector.isConnected()) {
+ logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
+ return;
+ }
+
+ synchronized (sequenceLock) {
+ try {
+ String commandStr = command.toString();
+ switch (channel) {
+ case CHANNEL_POWER:
+ if (command instanceof OnOffType) {
+ connector.sendCommand(
+ command == OnOffType.ON ? OppoCommand.POWER_ON : OppoCommand.POWER_OFF);
+ isPowerOn = (command == OnOffType.ON ? true : false);
+ }
+ break;
+ case CHANNEL_VOLUME:
+ if (command instanceof PercentType) {
+ connector.sendCommand(OppoCommand.SET_VOLUME_LEVEL, commandStr);
+ }
+ break;
+ case CHANNEL_MUTE:
+ if (command instanceof OnOffType) {
+ if (command == OnOffType.ON) {
+ connector.sendCommand(OppoCommand.SET_VOLUME_LEVEL, MUTE);
+ } else {
+ connector.sendCommand(OppoCommand.MUTE);
+ }
+ }
+ break;
+ case CHANNEL_SOURCE:
+ if (command instanceof DecimalType) {
+ int value = ((DecimalType) command).intValue();
+ connector.sendCommand(OppoCommand.SET_INPUT_SOURCE, String.valueOf(value));
+ }
+ break;
+ case CHANNEL_CONTROL:
+ this.handleControlCommand(command);
+ break;
+ case CHANNEL_TIME_MODE:
+ if (command instanceof StringType) {
+ connector.sendCommand(OppoCommand.SET_TIME_DISPLAY, commandStr);
+ currentTimeMode = commandStr;
+ }
+ break;
+ case CHANNEL_REPEAT_MODE:
+ if (command instanceof StringType) {
+ // this one is lame, the response code when querying repeat mode is two digits,
+ // but setting it is a 2-3 letter code.
+ connector.sendCommand(OppoCommand.SET_REPEAT, OppoStatusCodes.REPEAT_MODE.get(commandStr));
+ }
+ break;
+ case CHANNEL_ZOOM_MODE:
+ if (command instanceof StringType) {
+ // again why could't they make the query code and set code the same?
+ connector.sendCommand(OppoCommand.SET_ZOOM_RATIO,
+ OppoStatusCodes.ZOOM_MODE.get(commandStr));
+ }
+ break;
+ case CHANNEL_SUB_SHIFT:
+ if (command instanceof DecimalType) {
+ int value = ((DecimalType) command).intValue();
+ connector.sendCommand(OppoCommand.SET_SUBTITLE_SHIFT, String.valueOf(value));
+ }
+ break;
+ case CHANNEL_OSD_POSITION:
+ if (command instanceof DecimalType) {
+ int value = ((DecimalType) command).intValue();
+ connector.sendCommand(OppoCommand.SET_OSD_POSITION, String.valueOf(value));
+ }
+ break;
+ case CHANNEL_HDMI_MODE:
+ if (command instanceof StringType) {
+ connector.sendCommand(OppoCommand.SET_HDMI_MODE, commandStr);
+ }
+ break;
+ case CHANNEL_HDR_MODE:
+ if (command instanceof StringType) {
+ connector.sendCommand(OppoCommand.SET_HDR_MODE, commandStr);
+ }
+ break;
+ case CHANNEL_REMOTE_BUTTON:
+ if (command instanceof StringType) {
+ connector.sendCommand(commandStr);
+ }
+ break;
+ default:
+ logger.warn("Unknown Command {} from channel {}", command, channel);
+ break;
+ }
+ } catch (OppoException e) {
+ logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
+ closeConnection();
+ scheduleReconnectJob();
+ }
+ }
+ }
+
+ /**
+ * Open the connection with the Oppo player
+ *
+ * @return true if the connection is opened successfully or false if not
+ */
+ private synchronized boolean openConnection() {
+ connector.addEventListener(this);
+ try {
+ connector.open();
+ } catch (OppoException e) {
+ logger.debug("openConnection() failed: {}", e.getMessage());
+ }
+ logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
+ return connector.isConnected();
+ }
+
+ /**
+ * Close the connection with the Oppo player
+ */
+ private synchronized void closeConnection() {
+ if (connector.isConnected()) {
+ connector.close();
+ connector.removeEventListener(this);
+ logger.debug("closeConnection(): disconnected");
+ }
+ }
+
+ /**
+ * Handle an event received from the Oppo player
+ *
+ * @param event the event to process
+ */
+ @Override
+ public void onNewMessageEvent(OppoMessageEvent evt) {
+ logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
+ lastEventReceived = System.currentTimeMillis();
+
+ String key = evt.getKey();
+ String updateData = evt.getValue().trim();
+ if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
+ updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
+ }
+
+ synchronized (sequenceLock) {
+ try {
+ switch (key) {
+ case NOP: // ignore
+ break;
+ case UTC:
+ // Player sent a time code update ie: 000 000 T 00:00:01
+ // g1 = title(movie only; cd always 000), g2 = chapter(movie)/track(cd), g3 = time display code,
+ // g4 = time
+ Matcher matcher = TIME_CODE_PATTERN.matcher(updateData);
+ if (matcher.find()) {
+ // only update these when chapter/track changes to prevent spamming the channels with
+ // unnecessary updates
+ if (!currentChapter.equals(matcher.group(2))) {
+ currentChapter = matcher.group(2);
+ // for CDs this will get track 1/x also
+ connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK);
+ // for movies shows chapter 1/x; always 0/0 for CDs
+ connector.sendCommand(OppoCommand.QUERY_CHAPTER);
+ }
+
+ if (!currentTimeMode.equals(matcher.group(3))) {
+ currentTimeMode = matcher.group(3);
+ updateChannelState(CHANNEL_TIME_MODE, currentTimeMode);
+ }
+ updateChannelState(CHANNEL_TIME_DISPLAY, matcher.group(4));
+ } else {
+ logger.debug("no match on message: {}", updateData);
+ }
+ break;
+ case QTE:
+ case QTR:
+ case QCE:
+ case QCR:
+ // these are used with verbose mode 2
+ updateChannelState(CHANNEL_TIME_DISPLAY, updateData);
+ break;
+ case QVR:
+ this.versionString = updateData;
+ break;
+ case QPW:
+ updateChannelState(CHANNEL_POWER, updateData);
+ if (OFF.equals(updateData)) {
+ currentPlayMode = BLANK;
+ isPowerOn = false;
+ } else {
+ isPowerOn = true;
+ }
+ break;
+ case UPW:
+ updateChannelState(CHANNEL_POWER, ONE.equals(updateData) ? ON : OFF);
+ if (ZERO.equals(updateData)) {
+ currentPlayMode = BLANK;
+ isPowerOn = false;
+ } else {
+ isPowerOn = true;
+ }
+ break;
+ case QVL:
+ case UVL:
+ case VUP:
+ case VDN:
+ if (MUTE.equals(updateData) || MUT.equals(updateData)) { // query sends MUTE, update sends MUT
+ updateChannelState(CHANNEL_MUTE, ON);
+ } else if (UMT.equals(updateData)) {
+ updateChannelState(CHANNEL_MUTE, OFF);
+ } else {
+ updateChannelState(CHANNEL_VOLUME, updateData);
+ updateChannelState(CHANNEL_MUTE, OFF);
+ }
+ break;
+ case QIS:
+ case UIS:
+ // example: 0 BD-PLAYER, split off just the number
+ updateChannelState(CHANNEL_SOURCE, updateData.split(SPACE)[0]);
+ break;
+ case UPL:
+ // we got the playback status update, throw it away and call the query because the text output
+ // is better
+ connector.sendCommand(OppoCommand.QUERY_PLAYBACK_STATUS);
+ break;
+ case QTK:
+ // example: 02/10, split off both numbers
+ String[] track = updateData.split(SLASH);
+ if (track.length == 2) {
+ updateChannelState(CHANNEL_CURRENT_TITLE, track[0]);
+ updateChannelState(CHANNEL_TOTAL_TITLE, track[1]);
+ }
+ break;
+ case QCH:
+ // example: 03/03, split off the both numbers
+ String[] chapter = updateData.split(SLASH);
+ if (chapter.length == 2) {
+ updateChannelState(CHANNEL_CURRENT_CHAPTER, chapter[0]);
+ updateChannelState(CHANNEL_TOTAL_CHAPTER, chapter[1]);
+ }
+ break;
+ case QPL:
+ // if playback has stopped, we have to zero out Time, Title and Track info and so on manually
+ if (NO_DISC.equals(updateData) || LOADING.equals(updateData) || OPEN.equals(updateData)
+ || CLOSE.equals(updateData) || STOP.equals(updateData)) {
+ updateChannelState(CHANNEL_CURRENT_TITLE, ZERO);
+ updateChannelState(CHANNEL_TOTAL_TITLE, ZERO);
+ updateChannelState(CHANNEL_CURRENT_CHAPTER, ZERO);
+ updateChannelState(CHANNEL_TOTAL_CHAPTER, ZERO);
+ updateChannelState(CHANNEL_TIME_DISPLAY, UNDEF);
+ updateChannelState(CHANNEL_AUDIO_TYPE, UNDEF);
+ updateChannelState(CHANNEL_SUBTITLE_TYPE, UNDEF);
+ }
+ updateChannelState(CHANNEL_PLAY_MODE, updateData);
+
+ // if switching to play mode and not a CD then query the subtitle type...
+ // because if subtitles were on when playback stopped, they got nulled out above
+ // and the subtitle update message ("UST") is not sent when play starts like it is for audio
+ if (PLAY.equals(updateData) && !CDDA.equals(currentDiscType)) {
+ connector.sendCommand(OppoCommand.QUERY_SUBTITLE_TYPE);
+ }
+ currentPlayMode = updateData;
+ break;
+ case QRP:
+ updateChannelState(CHANNEL_REPEAT_MODE, updateData);
+ break;
+ case QZM:
+ updateChannelState(CHANNEL_ZOOM_MODE, updateData);
+ break;
+ case UDT:
+ // we got the disc type status update, throw it away
+ // and call the query because the text output is better
+ connector.sendCommand(OppoCommand.QUERY_DISC_TYPE);
+ case QDT:
+ currentDiscType = updateData;
+ updateChannelState(CHANNEL_DISC_TYPE, updateData);
+ break;
+ case UAT:
+ // we got the audio type status update, throw it away
+ // and call the query because the text output is better
+ connector.sendCommand(OppoCommand.QUERY_AUDIO_TYPE);
+ break;
+ case QAT:
+ updateChannelState(CHANNEL_AUDIO_TYPE, updateData);
+ break;
+ case UST:
+ // we got the subtitle type status update, throw it away
+ // and call the query because the text output is better
+ connector.sendCommand(OppoCommand.QUERY_SUBTITLE_TYPE);
+ break;
+ case QST:
+ updateChannelState(CHANNEL_SUBTITLE_TYPE, updateData);
+ break;
+ case UAR: // 203 & 205 only
+ updateChannelState(CHANNEL_ASPECT_RATIO, updateData);
+ break;
+ case UVO:
+ // example: _480I60 1080P60 - 1st source res, 2nd output res
+ String[] resolution = updateData.replace(UNDERSCORE, BLANK).split(SPACE);
+ if (resolution.length == 2) {
+ updateChannelState(CHANNEL_SOURCE_RESOLUTION, resolution[0]);
+ updateChannelState(CHANNEL_OUTPUT_RESOLUTION, resolution[1]);
+ }
+ break;
+ case U3D:
+ updateChannelState(CHANNEL_3D_INDICATOR, updateData);
+ break;
+ case QSH:
+ updateChannelState(CHANNEL_SUB_SHIFT, updateData);
+ break;
+ case QOP:
+ updateChannelState(CHANNEL_OSD_POSITION, updateData);
+ break;
+ case QHD:
+ if (this.isUDP20X) {
+ updateChannelState(CHANNEL_HDMI_MODE, updateData);
+ } else {
+ handleHdmiModeUpdate(updateData);
+ }
+ break;
+ case QHR: // 203 & 205 only
+ updateChannelState(CHANNEL_HDR_MODE, updateData);
+ break;
+ default:
+ logger.debug("onNewMessageEvent: unhandled key {}, value: {}", key, updateData);
+ break;
+ }
+ } catch (OppoException e) {
+ logger.debug("Exception processing event from player: {}", e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Schedule the reconnection job
+ */
+ private void scheduleReconnectJob() {
+ logger.debug("Schedule reconnect job");
+ cancelReconnectJob();
+
+ reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
+ if (!connector.isConnected()) {
+ logger.debug("Trying to reconnect...");
+ closeConnection();
+ String error = null;
+ synchronized (sequenceLock) {
+ if (openConnection()) {
+ try {
+ long prevUpdateTime = lastEventReceived;
+
+ connector.sendCommand(OppoCommand.SET_VERBOSE_MODE, this.verboseMode);
+ Thread.sleep(SLEEP_BETWEEN_CMD_MS);
+
+ // if the player is off most of these won't really do much...
+ OppoCommand.INITIAL_COMMANDS.forEach(cmd -> {
+ try {
+ connector.sendCommand(cmd);
+ Thread.sleep(SLEEP_BETWEEN_CMD_MS);
+ } catch (OppoException | InterruptedException e) {
+ logger.debug("Exception sending initial commands: {}", e.getMessage());
+ }
+ });
+
+ // prevUpdateTime should have changed if a message was received from the player
+ if (prevUpdateTime == lastEventReceived) {
+ error = "Player not responding to status requests";
+ }
+ } catch (OppoException | InterruptedException e) {
+ error = "First command after connection failed";
+ logger.debug("{}: {}", error, e.getMessage());
+ }
+ } else {
+ error = "Reconnection failed";
+ }
+ if (error != null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
+ closeConnection();
+ } else {
+ updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
+ }
+ }
+ }
+ }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Cancel the reconnection job
+ */
+ private void cancelReconnectJob() {
+ ScheduledFuture> reconnectJob = this.reconnectJob;
+ if (reconnectJob != null) {
+ reconnectJob.cancel(true);
+ this.reconnectJob = null;
+ }
+ }
+
+ /**
+ * Schedule the polling job
+ */
+ private void schedulePollingJob() {
+ logger.debug("Schedule polling job");
+ cancelPollingJob();
+
+ // when the Oppo is off, this will keep the connection (esp Serial over IP) alive and
+ // detect if the connection goes down
+ pollingJob = scheduler.scheduleWithFixedDelay(() -> {
+ if (connector.isConnected()) {
+ logger.debug("Polling the player for updated status...");
+
+ synchronized (sequenceLock) {
+ try {
+ // if using direct IP connection on the 83/9x/10x, no unsolicited updates are sent
+ // so we must query everything to know what changed.
+ if (isBdpIP) {
+ connector.sendCommand(OppoCommand.QUERY_POWER_STATUS);
+ if (isPowerOn) {
+ OppoCommand.QUERY_COMMANDS.forEach(cmd -> {
+ try {
+ connector.sendCommand(cmd);
+ Thread.sleep(SLEEP_BETWEEN_CMD_MS);
+ } catch (OppoException | InterruptedException e) {
+ logger.debug("Exception sending polling commands: {}", e.getMessage());
+ }
+ });
+ }
+ }
+
+ // for Verbose mode 2 get the current play back time if we are playing, otherwise just do NO_OP
+ if ((VERBOSE_2.equals(this.verboseMode) && PLAY.equals(currentPlayMode))
+ || (isBdpIP && isPowerOn)) {
+ switch (currentTimeMode) {
+ case T:
+ connector.sendCommand(OppoCommand.QUERY_TITLE_ELAPSED);
+ break;
+ case X:
+ connector.sendCommand(OppoCommand.QUERY_TITLE_REMAIN);
+ break;
+ case C:
+ connector.sendCommand(OppoCommand.QUERY_CHAPTER_ELAPSED);
+ break;
+ case K:
+ connector.sendCommand(OppoCommand.QUERY_CHAPTER_REMAIN);
+ break;
+ }
+ Thread.sleep(SLEEP_BETWEEN_CMD_MS);
+
+ // make queries to refresh total number of titles/tracks & chapters
+ connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK);
+ Thread.sleep(SLEEP_BETWEEN_CMD_MS);
+ connector.sendCommand(OppoCommand.QUERY_CHAPTER);
+ } else if (!isBdpIP) {
+ // verbose mode 3
+ connector.sendCommand(OppoCommand.NO_OP);
+ }
+
+ } catch (OppoException | InterruptedException e) {
+ logger.warn("Polling error: {}", e.getMessage());
+ }
+
+ // if the last event received was more than 1.25 intervals ago,
+ // the player is not responding even though the connection is still good
+ if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_SEC * 1.25 * 1000)) {
+ logger.debug("Player not responding to status requests");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Player not responding to status requests");
+ closeConnection();
+ scheduleReconnectJob();
+ }
+ }
+ }
+ }, INITIAL_POLLING_DELAY_SEC, 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;
+ }
+ }
+
+ /**
+ * Update the state of a channel
+ *
+ * @param channel the channel
+ * @param value the value to be updated
+ */
+ private void updateChannelState(String channel, String value) {
+ if (!isLinked(channel)) {
+ return;
+ }
+
+ if (UNDEF.equals(value)) {
+ updateState(channel, UnDefType.UNDEF);
+ return;
+ }
+
+ State state = UnDefType.UNDEF;
+
+ switch (channel) {
+ case CHANNEL_TIME_DISPLAY:
+ String[] timeArr = value.split(COLON);
+ if (timeArr.length == 3) {
+ int seconds = (Integer.parseInt(timeArr[0]) * 3600) + (Integer.parseInt(timeArr[1]) * 60)
+ + Integer.parseInt(timeArr[2]);
+ state = new QuantityType<>(seconds, SmartHomeUnits.SECOND);
+ } else {
+ state = UnDefType.UNDEF;
+ }
+ break;
+ case CHANNEL_POWER:
+ case CHANNEL_MUTE:
+ state = ON.equals(value) ? OnOffType.ON : OnOffType.OFF;
+ break;
+ case CHANNEL_SOURCE:
+ case CHANNEL_SUB_SHIFT:
+ case CHANNEL_OSD_POSITION:
+ case CHANNEL_CURRENT_TITLE:
+ case CHANNEL_TOTAL_TITLE:
+ case CHANNEL_CURRENT_CHAPTER:
+ case CHANNEL_TOTAL_CHAPTER:
+ state = new DecimalType(value);
+ break;
+ case CHANNEL_VOLUME:
+ state = new PercentType(BigDecimal.valueOf(Integer.parseInt(value)));
+ break;
+ case CHANNEL_PLAY_MODE:
+ case CHANNEL_TIME_MODE:
+ case CHANNEL_REPEAT_MODE:
+ case CHANNEL_ZOOM_MODE:
+ case CHANNEL_DISC_TYPE:
+ case CHANNEL_AUDIO_TYPE:
+ case CHANNEL_SUBTITLE_TYPE:
+ case CHANNEL_ASPECT_RATIO:
+ case CHANNEL_SOURCE_RESOLUTION:
+ case CHANNEL_OUTPUT_RESOLUTION:
+ case CHANNEL_3D_INDICATOR:
+ case CHANNEL_HDMI_MODE:
+ case CHANNEL_HDR_MODE:
+ state = new StringType(value);
+ break;
+ default:
+ break;
+ }
+ updateState(channel, state);
+ }
+
+ /**
+ * Handle a button press from a UI Player item
+ *
+ * @param command the control button press command received
+ */
+ private void handleControlCommand(Command command) throws OppoException {
+ if (command instanceof PlayPauseType) {
+ if (command == PlayPauseType.PLAY) {
+ connector.sendCommand(OppoCommand.PLAY);
+ } else if (command == PlayPauseType.PAUSE) {
+ connector.sendCommand(OppoCommand.PAUSE);
+ }
+ } else if (command instanceof NextPreviousType) {
+ if (command == NextPreviousType.NEXT) {
+ connector.sendCommand(OppoCommand.NEXT);
+ } else if (command == NextPreviousType.PREVIOUS) {
+ connector.sendCommand(OppoCommand.PREV);
+ }
+ } else if (command instanceof RewindFastforwardType) {
+ if (command == RewindFastforwardType.FASTFORWARD) {
+ connector.sendCommand(OppoCommand.FFORWARD);
+ } else if (command == RewindFastforwardType.REWIND) {
+ connector.sendCommand(OppoCommand.REWIND);
+ }
+ } else {
+ logger.warn("Unknown control command: {}", command);
+ }
+ }
+
+ private void buildOptionDropdowns(int model) {
+ if (model == MODEL83 || model == MODEL103 || model == MODEL105) {
+ hdmiModeOptions.add(new StateOption("AUTO", "Auto"));
+ hdmiModeOptions.add(new StateOption("SRC", "Source Direct"));
+ if (!(model == MODEL83)) {
+ hdmiModeOptions.add(new StateOption("4K2K", "4K*2K"));
+ }
+ hdmiModeOptions.add(new StateOption("1080P", "1080P"));
+ hdmiModeOptions.add(new StateOption("1080I", "1080I"));
+ hdmiModeOptions.add(new StateOption("720P", "720P"));
+ hdmiModeOptions.add(new StateOption("SDP", "480P"));
+ hdmiModeOptions.add(new StateOption("SDI", "480I"));
+ }
+
+ if (model == MODEL103 || model == MODEL105) {
+ inputSourceOptions.add(new StateOption("0", "Blu-Ray Player"));
+ inputSourceOptions.add(new StateOption("1", "HDMI/MHL IN-Front"));
+ inputSourceOptions.add(new StateOption("2", "HDMI IN-Back"));
+ inputSourceOptions.add(new StateOption("3", "ARC"));
+
+ if (model == MODEL105) {
+ inputSourceOptions.add(new StateOption("4", "Optical In"));
+ inputSourceOptions.add(new StateOption("5", "Coaxial In"));
+ inputSourceOptions.add(new StateOption("6", "USB Audio In"));
+ }
+ }
+
+ if (model == MODEL203 || model == MODEL205) {
+ hdmiModeOptions.add(new StateOption("AUTO", "Auto"));
+ hdmiModeOptions.add(new StateOption("SRC", "Source Direct"));
+ hdmiModeOptions.add(new StateOption("UHD_AUTO", "UHD Auto"));
+ hdmiModeOptions.add(new StateOption("UHD24", "UHD24"));
+ hdmiModeOptions.add(new StateOption("UHD50", "UHD50"));
+ hdmiModeOptions.add(new StateOption("UHD60", "UHD60"));
+ hdmiModeOptions.add(new StateOption("1080P_AUTO", "1080P Auto"));
+ hdmiModeOptions.add(new StateOption("1080P24", "1080P24"));
+ hdmiModeOptions.add(new StateOption("1080P50", "1080P50"));
+ hdmiModeOptions.add(new StateOption("1080P60", "1080P60"));
+ hdmiModeOptions.add(new StateOption("1080I50", "1080I50"));
+ hdmiModeOptions.add(new StateOption("1080I60", "1080I60"));
+ hdmiModeOptions.add(new StateOption("720P50", "720P50"));
+ hdmiModeOptions.add(new StateOption("720P60", "720P60"));
+ hdmiModeOptions.add(new StateOption("576P", "567P"));
+ hdmiModeOptions.add(new StateOption("576I", "567I"));
+ hdmiModeOptions.add(new StateOption("480P", "480P"));
+ hdmiModeOptions.add(new StateOption("480I", "480I"));
+
+ inputSourceOptions.add(new StateOption("0", "Blu-Ray Player"));
+ inputSourceOptions.add(new StateOption("1", "HDMI IN"));
+ inputSourceOptions.add(new StateOption("2", "ARC"));
+
+ if (model == MODEL205) {
+ inputSourceOptions.add(new StateOption("3", "Optical In"));
+ inputSourceOptions.add(new StateOption("4", "Coaxial In"));
+ inputSourceOptions.add(new StateOption("5", "USB Audio In"));
+ }
+ }
+ }
+
+ private void handleHdmiModeUpdate(String updateData) {
+ // ugly... a couple of the query hdmi mode response codes on the earlier models don't match the code to set it
+ // some of this protocol is weird like that...
+ if ("480I".equals(updateData)) {
+ updateChannelState(CHANNEL_HDMI_MODE, "SDI");
+ } else if ("480P".equals(updateData)) {
+ updateChannelState(CHANNEL_HDMI_MODE, "SDP");
+ } else if ("4K*2K".equals(updateData)) {
+ updateChannelState(CHANNEL_HDMI_MODE, "4K2K");
+ } else {
+ updateChannelState(CHANNEL_HDMI_MODE, updateData);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.oppo/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.oppo/src/main/resources/ESH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..e24e8e3bc2a46
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/resources/ESH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Oppo Blu-ray Player Binding
+ Controls the Oppo UDP-203/205 and BDP-83/93/95/103/105 Blu-ray Players
+ Michael Lobstein
+
+
diff --git a/bundles/org.openhab.binding.oppo/src/main/resources/ESH-INF/thing/channels.xml b/bundles/org.openhab.binding.oppo/src/main/resources/ESH-INF/thing/channels.xml
new file mode 100644
index 0000000000000..d7018bb332c5a
--- /dev/null
+++ b/bundles/org.openhab.binding.oppo/src/main/resources/ESH-INF/thing/channels.xml
@@ -0,0 +1,334 @@
+
+
+
+
+
+
+
+ An Oppo Blu-ray Disc Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Choose Model of Oppo Player
+ true
+
+
+
+
+
+
+
+
+
+ serial-port
+
+ Serial Port to Use for Connecting to the Oppo Player.
+
+
+ network-address
+
+ Host Name or IP Address of the Oppo Player or Machine Used for Serial Over IP.
+
+
+
+ (Optional) Communication Port for Serial Over IP Connection. Leave blank If Connecting Directly to the
+ Player.
+ true
+
+
+
+ If true, the player will send time updates every second. If false, the binding polls the player evey 30
+ seconds
+ false
+ true
+
+
+
+
+
+ Number
+
+ Select the Source Input for the Player
+
+
+
+ String
+
+ The Current Playback Mode of the Source
+
+
+
+
+ Player
+
+ Transport Controls e.g. Play/Pause/Next/Previous/Fast Forward/Rewind
+ Player
+
+
+
+ String
+
+ Sets the Time Information Display
+
+
+
+
+
+
+
+
+
+
+
+ Number:Time
+
+ The Playback Time Elapsed/Remaining in Seconds
+
+
+
+
+ Number
+
+ Current Title or Track Number Playing
+
+
+
+
+ Number
+
+ The Total Number of Titles or Tracks on the Disc
+
+
+
+
+ Number
+
+ Current Chapter Number
+
+
+
+
+ Number
+
+ The Total Number of Chapters in the Current Title
+
+
+
+
+ String
+
+ The Current Repeat Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ The Current Zoom Mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ The Current Type of Disc in the Player
+
+
+
+
+ String
+
+ The Current Audio Track Type
+
+
+
+
+ String
+
+ The Current Subtitle Selected
+
+
+
+
+ String
+
+ The Aspect Ratio of the Current Video Output
+
+
+
+
+ String
+
+ The Video Resolution of the Content Being Played
+
+
+
+
+ String
+
+ The Video Resolution of the Player Output
+
+
+
+
+ String
+
+ Indicates If the Content Playing is 2D or 3D
+
+
+
+
+ Number
+
+ Set the OSD Position 0 to 5
+
+
+
+
+ Number
+
+ Set the Subtitle Shift -10 to 10
+
+
+
+
+ String
+
+ The Current HDMI Output Mode
+
+
+
+ String
+
+ The Current HDR Output Mode
+
+
+
+
+
+
+
+
+
+
+ String
+
+ Simulate Pressing a Button on the Remote Control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index eb5715d3fae4a..309b6cf2ad26b 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -193,6 +193,7 @@
org.openhab.binding.openthermgatewayorg.openhab.binding.openuvorg.openhab.binding.openweathermap
+ org.openhab.binding.oppoorg.openhab.binding.orviboorg.openhab.binding.paradoxalarmorg.openhab.binding.pentair