added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.oppo</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -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

View File

@@ -0,0 +1,213 @@
# Oppo Blu-ray player Binding
![Oppo logo](doc/oppo.png)
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 the 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]
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.oppo</artifactId>
<name>openHAB Add-ons :: Bundles :: Oppo Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.oppo-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-oppo" description="Oppo Blu-ray Player Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.oppo/${project.version}</bundle>
</feature>
</features>

View File

@@ -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.openhab.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";
}

View File

@@ -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);
}
}

View File

@@ -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.openhab.binding.oppo.internal.handler.OppoHandler;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link 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<ThingTypeUID> 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;
}
}

View File

@@ -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.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.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;
}
}

View File

@@ -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 final String value;
public static final Set<OppoCommand> 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<OppoCommand> 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;
}
}

View File

@@ -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 final List<OppoMessageEventListener> 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();
this.readerThread = null;
try {
readerThread.join(3000);
} catch (InterruptedException e) {
logger.warn("Error joining readerThread: {}", e.getMessage());
}
}
}
/**
* 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));
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,126 @@
/**
* 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 final @Nullable String address;
private final int port;
private final String uid;
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
* @param uid the thing uid string
*/
public OppoIpConnector(@Nullable String address, int port, String uid) {
this.address = address;
this.port = port;
this.uid = uid;
}
@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.uid, 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);
}
}
}

View File

@@ -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 final String key;
private final 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;
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,84 @@
/**
* 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.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
* @param uid the thing uid string
* @param connectionId a string that uniquely identifies the particular connection
*/
public OppoReaderThread(OppoConnector connector, String uid, String connectionId) {
super("OH-binding-" + uid + "-" + connectionId);
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");
}
}

View File

@@ -0,0 +1,127 @@
/**
* 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.openhab.binding.oppo.internal.OppoException;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 final String serialPortName;
private final SerialPortManager serialPortManager;
private final String uid;
private @Nullable SerialPort serialPort;
/**
* Constructor
*
* @param serialPortManager the serial port manager
* @param serialPortName the serial port name to be used
* @param uid the thing uid string
*/
public OppoSerialConnector(SerialPortManager serialPortManager, String serialPortName, String uid) {
this.serialPortManager = serialPortManager;
this.serialPortName = serialPortName;
this.uid = uid;
}
@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.uid, 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");
}
}

View File

@@ -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<String, String> 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<String, String> 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");
}
}

View File

@@ -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;
}

View File

@@ -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.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
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<ThingTypeUID> 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<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES_UIDS;
}
/**
* {@inheritDoc}
*
* Starts the scan. This discovery will:
* <ul>
* <li>Request all the network interfaces</li>
* <li>For each network interface, create a listening thread using {@link #executorService}</li>
* <li>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</li>
* <li>The {@link DatagramPacket} is then investigated to see if is a SDDP packet and will create a new thing from
* it</li>
* </ul>
* 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<NetworkInterface> 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
*
* <pre>
* Notify: OPPO Player Start
* Server IP: 192.168.0.2
* Server Port: 23
* Server Name: OPPO UDP-203
* </pre>
*
*
* @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("<modelName>OPPO BDP-103</modelName>")) {
model = MODEL103;
displayName = DISPLAY_NAME_103;
} else if (result != null && result.contains("<modelName>OPPO BDP-105</modelName>")) {
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<String, Object> 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;
}
}

View File

@@ -0,0 +1,882 @@
/**
* 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.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.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
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<StateOption> inputSourceOptions = new ArrayList<>();
private List<StateOption> 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);
final String uid = this.getThing().getUID().getAsString();
// 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, uid);
} else if (port != null) {
connector = new OppoIpConnector(host, port, uid);
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<Channel> 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);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="oppo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Oppo Blu-ray Player Binding</name>
<description>Controls the Oppo UDP-203/205 and BDP-83/93/95/103/105 Blu-ray Players</description>
<author>Michael Lobstein</author>
</binding:binding>

View File

@@ -0,0 +1,334 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="oppo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Oppo Blu-ray disc player Thing -->
<thing-type id="player">
<label>Oppo</label>
<description>
An Oppo Blu-ray Disc Player
</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="volume" typeId="system.volume"/>
<channel id="mute" typeId="system.mute"/>
<channel id="source" typeId="source"/>
<channel id="play_mode" typeId="play_mode"/>
<channel id="control" typeId="control"/>
<channel id="time_mode" typeId="time_mode"/>
<channel id="time_display" typeId="time_display"/>
<channel id="current_title" typeId="current_title"/>
<channel id="total_title" typeId="total_title"/>
<channel id="current_chapter" typeId="current_chapter"/>
<channel id="total_chapter" typeId="total_chapter"/>
<channel id="repeat_mode" typeId="repeat_mode"/>
<channel id="zoom_mode" typeId="zoom_mode"/>
<channel id="disc_type" typeId="disc_type"/>
<channel id="audio_type" typeId="audio_type"/>
<channel id="subtitle_type" typeId="subtitle_type"/>
<channel id="aspect_ratio" typeId="aspect_ratio"/>
<channel id="source_resolution" typeId="source_resolution"/>
<channel id="output_resolution" typeId="output_resolution"/>
<channel id="3d_indicator" typeId="3d_indicator"/>
<channel id="sub_shift" typeId="sub_shift"/>
<channel id="osd_position" typeId="osd_position"/>
<channel id="hdmi_mode" typeId="hdmi_mode"/>
<channel id="hdr_mode" typeId="hdr_mode"/>
<channel id="remote_button" typeId="remote_button"/>
</channels>
<config-description>
<parameter name="model" type="integer" required="true">
<label>Player Model</label>
<description>Choose Model of Oppo Player</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="83">Oppo BDP-83 or BDP-93/95</option>
<option value="103">Oppo BDP-103/103D</option>
<option value="105">Oppo BDP-105/105D</option>
<option value="203">Oppo UDP-203</option>
<option value="205">Oppo UDP-205</option>
</options>
</parameter>
<parameter name="serialPort" type="text" required="false">
<context>serial-port</context>
<label>Serial Port</label>
<description>Serial Port to Use for Connecting to the Oppo Player.</description>
</parameter>
<parameter name="host" type="text" required="false">
<context>network-address</context>
<label>Address</label>
<description>Host Name or IP Address of the Oppo Player or Machine Used for Serial Over IP.</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="false">
<label>Port</label>
<description>(Optional) Communication Port for Serial Over IP Connection. Leave blank If Connecting Directly to the
Player.</description>
<advanced>true</advanced>
</parameter>
<parameter name="verboseMode" type="boolean" required="false">
<label>Verbose Mode</label>
<description>If true, the player will send time updates every second. If false, the binding polls the player evey 30
seconds</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="source">
<item-type>Number</item-type>
<label>Source Input</label>
<description>Select the Source Input for the Player</description>
</channel-type>
<channel-type id="play_mode">
<item-type>String</item-type>
<label>Play Mode</label>
<description>The Current Playback Mode of the Source</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="control">
<item-type>Player</item-type>
<label>Control</label>
<description>Transport Controls e.g. Play/Pause/Next/Previous/Fast Forward/Rewind</description>
<category>Player</category>
</channel-type>
<channel-type id="time_mode">
<item-type>String</item-type>
<label>Time Display Mode</label>
<description>Sets the Time Information Display</description>
<state>
<options>
<option value="T">Title Elapsed Time</option>
<option value="X">Title Remaining Time</option>
<option value="C">Chapter/Track Elapsed Time</option>
<option value="K">Chapter/Track Remaining Time</option>
</options>
</state>
</channel-type>
<channel-type id="time_display">
<item-type>Number:Time</item-type>
<label>Time Display (S)</label>
<description>The Playback Time Elapsed/Remaining in Seconds</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="current_title">
<item-type>Number</item-type>
<label>Current Title/Track</label>
<description>Current Title or Track Number Playing</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="total_title">
<item-type>Number</item-type>
<label>Total Number of Titles/Tracks</label>
<description>The Total Number of Titles or Tracks on the Disc</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="current_chapter">
<item-type>Number</item-type>
<label>Current Chapter</label>
<description>Current Chapter Number</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="total_chapter">
<item-type>Number</item-type>
<label>Total Number of Chapters</label>
<description>The Total Number of Chapters in the Current Title</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="repeat_mode">
<item-type>String</item-type>
<label>Repeat Mode</label>
<description>The Current Repeat Mode</description>
<state>
<options>
<option value="00">Off</option>
<option value="02">Repeat Chapter</option>
<option value="03">Repeat All</option>
<option value="04">Repeat Title</option>
<option value="05">Shuffle</option>
<option value="06">Random</option>
</options>
</state>
</channel-type>
<channel-type id="zoom_mode">
<item-type>String</item-type>
<label>Zoom Mode</label>
<description>The Current Zoom Mode</description>
<state>
<options>
<option value="00">Off</option>
<option value="01">Stretch</option>
<option value="02">Full Screen</option>
<option value="03">Underscan</option>
<option value="04">1.2x</option>
<option value="05">1.3x</option>
<option value="06">1.5x</option>
<option value="07">2x</option>
<option value="08">3x</option>
<option value="09">4x</option>
<option value="10">1/2</option>
<option value="11">1/3</option>
<option value="12">1/4</option>
</options>
</state>
</channel-type>
<channel-type id="disc_type">
<item-type>String</item-type>
<label>Disc Type</label>
<description>The Current Type of Disc in the Player</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="audio_type">
<item-type>String</item-type>
<label>Audio Type</label>
<description>The Current Audio Track Type</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="subtitle_type">
<item-type>String</item-type>
<label>Subtitle Type</label>
<description>The Current Subtitle Selected</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="aspect_ratio">
<item-type>String</item-type>
<label>Aspect Ratio</label>
<description>The Aspect Ratio of the Current Video Output</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="source_resolution">
<item-type>String</item-type>
<label>Source Video Resolution</label>
<description>The Video Resolution of the Content Being Played</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="output_resolution">
<item-type>String</item-type>
<label>Output Video Resolution</label>
<description>The Video Resolution of the Player Output</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="3d_indicator">
<item-type>String</item-type>
<label>2D/3D Indicator</label>
<description>Indicates If the Content Playing is 2D or 3D</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="osd_position">
<item-type>Number</item-type>
<label>OSD Position</label>
<description>Set the OSD Position 0 to 5</description>
<state min="0" max="5" step="1" pattern="%d"/>
</channel-type>
<channel-type id="sub_shift">
<item-type>Number</item-type>
<label>Subtitle Shift</label>
<description>Set the Subtitle Shift -10 to 10</description>
<state min="-10" max="10" step="1" pattern="%d"/>
</channel-type>
<channel-type id="hdmi_mode">
<item-type>String</item-type>
<label>HDMI Mode</label>
<description>The Current HDMI Output Mode</description>
</channel-type>
<channel-type id="hdr_mode">
<item-type>String</item-type>
<label>HDR Mode</label>
<description>The Current HDR Output Mode</description>
<state>
<options>
<option value="Auto">Auto</option>
<option value="On">On</option>
<option value="Off">Off</option>
</options>
</state>
</channel-type>
<channel-type id="remote_button">
<item-type>String</item-type>
<label>Remote Button</label>
<description>Simulate Pressing a Button on the Remote Control</description>
<state>
<options>
<option value="EJT">Eject</option>
<option value="DIM">Dimmer</option>
<option value="PUR">Pure Audio</option>
<option value="VUP">Vol +</option>
<option value="VDN">Vol -</option>
<option value="MUT">Mute</option>
<option value="NU1">1</option>
<option value="NU2">2</option>
<option value="NU3">3</option>
<option value="NU4">4</option>
<option value="NU5">5</option>
<option value="NU6">6</option>
<option value="NU7">7</option>
<option value="NU8">8</option>
<option value="NU9">9</option>
<option value="NU0">0</option>
<option value="CLR">Clear</option>
<option value="GOT">Goto</option>
<option value="HOM">Home</option>
<option value="PUP">Page Up</option>
<option value="PDN">Page Down</option>
<option value="OSD">Info/Display</option>
<option value="TTL">Top Menu</option>
<option value="MNU">Pop-Up Menu</option>
<option value="NUP">Up</option>
<option value="NLT">Left</option>
<option value="NRT">Right</option>
<option value="NDN">Down</option>
<option value="SEL">Select</option>
<option value="SET">Setup</option>
<option value="RET">Return</option>
<option value="RED">Red</option>
<option value="GRN">Green</option>
<option value="BLU">Blue</option>
<option value="YLW">Yellow</option>
<option value="STP">Stop</option>
<option value="PLA">Play</option>
<option value="PAU">Pause</option>
<option value="PRE">Previous</option>
<option value="REV">Fast Reverse</option>
<option value="FWD">Fast Forward</option>
<option value="NXT">Next</option>
<option value="AUD">Audio</option>
<option value="SUB">Subtitle</option>
<option value="ANG">Angle</option>
<option value="ZOM">Zoom</option>
<option value="SAP">SAP</option>
<option value="ATB">AB Replay</option>
<option value="RPT">Repeat</option>
<option value="PIP">Picture in Picture</option>
<option value="HDM">HDMI Mode</option>
<option value="NFX">Netflix</option>
<option value="INH">Extended OSD</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>