added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user