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.bundles org.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.openthermgateway org.openhab.binding.openuv org.openhab.binding.openweathermap + org.openhab.binding.oppo org.openhab.binding.orvibo org.openhab.binding.paradoxalarm org.openhab.binding.pentair