2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.oppo.internal.handler;
15 import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
18 import java.math.BigDecimal;
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.oppo.internal.OppoException;
29 import org.openhab.binding.oppo.internal.OppoStateDescriptionOptionProvider;
30 import org.openhab.binding.oppo.internal.communication.OppoCommand;
31 import org.openhab.binding.oppo.internal.communication.OppoConnector;
32 import org.openhab.binding.oppo.internal.communication.OppoDefaultConnector;
33 import org.openhab.binding.oppo.internal.communication.OppoIpConnector;
34 import org.openhab.binding.oppo.internal.communication.OppoMessageEvent;
35 import org.openhab.binding.oppo.internal.communication.OppoMessageEventListener;
36 import org.openhab.binding.oppo.internal.communication.OppoSerialConnector;
37 import org.openhab.binding.oppo.internal.communication.OppoStatusCodes;
38 import org.openhab.binding.oppo.internal.configuration.OppoThingConfiguration;
39 import org.openhab.core.io.transport.serial.SerialPortManager;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.NextPreviousType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.PercentType;
44 import org.openhab.core.library.types.PlayPauseType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.RewindFastforwardType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.unit.Units;
49 import org.openhab.core.thing.Channel;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.State;
57 import org.openhab.core.types.StateOption;
58 import org.openhab.core.types.UnDefType;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * The {@link OppoHandler} is responsible for handling commands, which are sent to one of the channels.
65 * Based on the Rotel binding by Laurent Garnier
67 * @author Michael Lobstein - Initial contribution
70 public class OppoHandler extends BaseThingHandler implements OppoMessageEventListener {
71 private static final long RECON_POLLING_INTERVAL_SEC = 60;
72 private static final long POLLING_INTERVAL_SEC = 10;
73 private static final long INITIAL_POLLING_DELAY_SEC = 5;
74 private static final long SLEEP_BETWEEN_CMD_MS = 100;
76 private static final Pattern TIME_CODE_PATTERN = Pattern
77 .compile("^(\\d{3}) (\\d{3}) ([A-Z]{1}) (\\d{2}:\\d{2}:\\d{2})$");
79 private final Logger logger = LoggerFactory.getLogger(OppoHandler.class);
81 private @Nullable ScheduledFuture<?> reconnectJob;
82 private @Nullable ScheduledFuture<?> pollingJob;
84 private OppoStateDescriptionOptionProvider stateDescriptionProvider;
85 private SerialPortManager serialPortManager;
86 private OppoConnector connector = new OppoDefaultConnector();
88 private List<StateOption> inputSourceOptions = new ArrayList<>();
89 private List<StateOption> hdmiModeOptions = new ArrayList<>();
91 private long lastEventReceived = System.currentTimeMillis();
92 private String verboseMode = VERBOSE_2;
93 private String currentChapter = BLANK;
94 private String currentTimeMode = T;
95 private String currentPlayMode = BLANK;
96 private String currentDiscType = BLANK;
97 private boolean isPowerOn = false;
98 private boolean isUDP20X = false;
99 private boolean isBdpIP = false;
100 private boolean isVbModeSet = false;
101 private boolean isInitialQuery = false;
102 private Object sequenceLock = new Object();
107 public OppoHandler(Thing thing, OppoStateDescriptionOptionProvider stateDescriptionProvider,
108 SerialPortManager serialPortManager) {
110 this.stateDescriptionProvider = stateDescriptionProvider;
111 this.serialPortManager = serialPortManager;
115 public void initialize() {
116 OppoThingConfiguration config = getConfigAs(OppoThingConfiguration.class);
117 final String uid = this.getThing().getUID().getAsString();
119 // Check configuration settings
120 String configError = null;
121 boolean override = false;
123 Integer model = config.model;
124 String serialPort = config.serialPort;
125 String host = config.host;
126 Integer port = config.port;
129 configError = "player model must be specified";
133 if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
134 configError = "undefined serialPort and host configuration settings; please set one of them";
135 } else if (serialPort != null && (host == null || host.isEmpty())) {
136 if (serialPort.toLowerCase().startsWith("rfc2217")) {
137 configError = "use host and port configuration settings for a serial over IP connection";
141 if (model == MODEL83) {
145 } else if (model == MODEL103 || model == MODEL105) {
152 } else if (port <= 0) {
153 configError = "invalid port configuration setting";
157 if (configError != null) {
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
162 if (serialPort != null) {
163 connector = new OppoSerialConnector(serialPortManager, serialPort, uid);
164 } else if (port != null) {
165 connector = new OppoIpConnector(host, port, uid);
166 connector.overrideCmdPreamble(override);
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
169 "Either Serial port or Host & Port must be specifed");
173 if (config.verboseMode) {
174 this.verboseMode = VERBOSE_3;
177 if (model == MODEL203 || model == MODEL205) {
178 this.isUDP20X = true;
181 this.buildOptionDropdowns(model);
182 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
184 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_HDMI_MODE),
187 // remove channels not needed for this model
188 List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
190 if (model == MODEL83) {
191 channels.removeIf(c -> (c.getUID().getId().equals(CHANNEL_SUB_SHIFT)
192 || c.getUID().getId().equals(CHANNEL_OSD_POSITION)));
195 if (model == MODEL83 || model == MODEL103 || model == MODEL105) {
196 channels.removeIf(c -> (c.getUID().getId().equals(CHANNEL_ASPECT_RATIO)
197 || c.getUID().getId().equals(CHANNEL_HDR_MODE)));
200 // no query to determine this, so set the default value at startup
201 updateChannelState(CHANNEL_TIME_MODE, currentTimeMode);
203 updateThing(editThing().withChannels(channels).build());
205 scheduleReconnectJob();
206 schedulePollingJob();
208 updateStatus(ThingStatus.UNKNOWN);
212 public void dispose() {
213 cancelReconnectJob();
220 * Handle a command the UI
222 * @param channelUID the channel sending the command
223 * @param command the command received
227 public void handleCommand(ChannelUID channelUID, Command command) {
228 String channel = channelUID.getId();
230 if (getThing().getStatus() != ThingStatus.ONLINE) {
231 logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
235 if (!connector.isConnected()) {
236 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
240 synchronized (sequenceLock) {
242 String commandStr = command.toString();
245 if (command instanceof OnOffType) {
246 connector.sendCommand(
247 command == OnOffType.ON ? OppoCommand.POWER_ON : OppoCommand.POWER_OFF);
249 // set the power flag to false only, will be set true by QPW or UPW messages
250 if (command == OnOffType.OFF) {
252 isInitialQuery = false;
257 if (command instanceof PercentType) {
258 connector.sendCommand(OppoCommand.SET_VOLUME_LEVEL, commandStr);
262 if (command instanceof OnOffType) {
263 if (command == OnOffType.ON) {
264 connector.sendCommand(OppoCommand.SET_VOLUME_LEVEL, MUTE);
266 connector.sendCommand(OppoCommand.MUTE);
271 if (command instanceof DecimalType decimalCommand) {
272 int value = decimalCommand.intValue();
273 connector.sendCommand(OppoCommand.SET_INPUT_SOURCE, String.valueOf(value));
276 case CHANNEL_CONTROL:
277 this.handleControlCommand(command);
279 case CHANNEL_TIME_MODE:
280 if (command instanceof StringType) {
281 connector.sendCommand(OppoCommand.SET_TIME_DISPLAY, commandStr);
282 currentTimeMode = commandStr;
285 case CHANNEL_REPEAT_MODE:
286 if (command instanceof StringType) {
287 // this one is lame, the response code when querying repeat mode is two digits,
288 // but setting it is a 2-3 letter code.
289 connector.sendCommand(OppoCommand.SET_REPEAT, OppoStatusCodes.REPEAT_MODE.get(commandStr));
292 case CHANNEL_ZOOM_MODE:
293 if (command instanceof StringType) {
294 // again why could't they make the query code and set code the same?
295 connector.sendCommand(OppoCommand.SET_ZOOM_RATIO,
296 OppoStatusCodes.ZOOM_MODE.get(commandStr));
299 case CHANNEL_SUB_SHIFT:
300 if (command instanceof DecimalType decimalCommand) {
301 int value = decimalCommand.intValue();
302 connector.sendCommand(OppoCommand.SET_SUBTITLE_SHIFT, String.valueOf(value));
305 case CHANNEL_OSD_POSITION:
306 if (command instanceof DecimalType decimalCommand) {
307 int value = decimalCommand.intValue();
308 connector.sendCommand(OppoCommand.SET_OSD_POSITION, String.valueOf(value));
311 case CHANNEL_HDMI_MODE:
312 if (command instanceof StringType) {
313 connector.sendCommand(OppoCommand.SET_HDMI_MODE, commandStr);
316 case CHANNEL_HDR_MODE:
317 if (command instanceof StringType) {
318 connector.sendCommand(OppoCommand.SET_HDR_MODE, commandStr);
321 case CHANNEL_REMOTE_BUTTON:
322 if (command instanceof StringType) {
323 connector.sendCommand(commandStr);
327 logger.warn("Unknown Command {} from channel {}", command, channel);
330 } catch (OppoException e) {
331 logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
332 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
334 scheduleReconnectJob();
340 * Open the connection with the Oppo player
342 * @return true if the connection is opened successfully or false if not
344 private synchronized boolean openConnection() {
345 connector.addEventListener(this);
348 } catch (OppoException e) {
349 logger.debug("openConnection() failed: {}", e.getMessage());
351 logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
352 return connector.isConnected();
356 * Close the connection with the Oppo player
358 private synchronized void closeConnection() {
359 if (connector.isConnected()) {
361 connector.removeEventListener(this);
362 logger.debug("closeConnection(): disconnected");
367 * Handle an event received from the Oppo player
369 * @param event the event to process
372 public void onNewMessageEvent(OppoMessageEvent evt) {
373 logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
374 lastEventReceived = System.currentTimeMillis();
376 String key = evt.getKey();
377 String updateData = evt.getValue().trim();
378 if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
379 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
382 synchronized (sequenceLock) {
388 // Player sent a time code update ie: 000 000 T 00:00:01
389 // g1 = title(movie only; cd always 000), g2 = chapter(movie)/track(cd), g3 = time display code,
391 Matcher matcher = TIME_CODE_PATTERN.matcher(updateData);
392 if (matcher.find()) {
393 // only update these when chapter/track changes to prevent spamming the channels with
394 // unnecessary updates
395 if (!currentChapter.equals(matcher.group(2))) {
396 currentChapter = matcher.group(2);
397 // for CDs this will get track 1/x also
398 connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK);
399 // for movies shows chapter 1/x; always 0/0 for CDs
400 connector.sendCommand(OppoCommand.QUERY_CHAPTER);
403 if (!currentTimeMode.equals(matcher.group(3))) {
404 currentTimeMode = matcher.group(3);
405 updateChannelState(CHANNEL_TIME_MODE, currentTimeMode);
407 updateChannelState(CHANNEL_TIME_DISPLAY, matcher.group(4));
409 logger.debug("no match on message: {}", updateData);
416 // these are used with verbose mode 2
417 updateChannelState(CHANNEL_TIME_DISPLAY, updateData);
420 thing.setProperty(PROPERTY_FIRMWARE_VERSION, updateData);
423 updateChannelState(CHANNEL_POWER, updateData);
424 if (OFF.equals(updateData)) {
425 currentPlayMode = BLANK;
432 updateChannelState(CHANNEL_POWER, ONE.equals(updateData) ? ON : OFF);
433 if (ZERO.equals(updateData)) {
434 currentPlayMode = BLANK;
436 isInitialQuery = false;
445 if (MUTE.equals(updateData) || MUT.equals(updateData)) { // query sends MUTE, update sends MUT
446 updateChannelState(CHANNEL_MUTE, ON);
447 } else if (UMT.equals(updateData)) {
448 updateChannelState(CHANNEL_MUTE, OFF);
450 updateChannelState(CHANNEL_VOLUME, updateData);
451 updateChannelState(CHANNEL_MUTE, OFF);
456 // example: 0 BD-PLAYER, split off just the number
457 updateChannelState(CHANNEL_SOURCE, updateData.split(SPACE)[0]);
460 // example: 02/10, split off both numbers
461 String[] track = updateData.split(SLASH);
462 if (track.length == 2) {
463 updateChannelState(CHANNEL_CURRENT_TITLE, track[0]);
464 updateChannelState(CHANNEL_TOTAL_TITLE, track[1]);
468 // example: 03/03, split off the both numbers
469 String[] chapter = updateData.split(SLASH);
470 if (chapter.length == 2) {
471 updateChannelState(CHANNEL_CURRENT_CHAPTER, chapter[0]);
472 updateChannelState(CHANNEL_TOTAL_CHAPTER, chapter[1]);
477 // try to normalize the slightly different responses between UPL and QPL
478 String playStatus = OppoStatusCodes.PLAYBACK_STATUS.get(updateData);
479 if (playStatus == null) {
480 playStatus = updateData;
483 // if playback has stopped, we have to zero out Time, Title and Track info and so on manually
484 if (NO_DISC.equals(playStatus) || LOADING.equals(playStatus) || OPEN.equals(playStatus)
485 || CLOSE.equals(playStatus) || STOP.equals(playStatus)) {
486 updateChannelState(CHANNEL_CURRENT_TITLE, ZERO);
487 updateChannelState(CHANNEL_TOTAL_TITLE, ZERO);
488 updateChannelState(CHANNEL_CURRENT_CHAPTER, ZERO);
489 updateChannelState(CHANNEL_TOTAL_CHAPTER, ZERO);
490 updateChannelState(CHANNEL_TIME_DISPLAY, UNDEF);
491 updateChannelState(CHANNEL_AUDIO_TYPE, UNDEF);
492 updateChannelState(CHANNEL_SUBTITLE_TYPE, UNDEF);
494 updateChannelState(CHANNEL_PLAY_MODE, playStatus);
496 // ejecting the disc does not produce a UDT message, so clear disc type manually
497 if (OPEN.equals(playStatus) || NO_DISC.equals(playStatus)) {
498 updateChannelState(CHANNEL_DISC_TYPE, UNKNOW_DISC);
499 currentDiscType = BLANK;
502 // if switching to play mode and not a CD then query the subtitle type...
503 // because if subtitles were on when playback stopped, they got nulled out above
504 // and the subtitle update message ("UST") is not sent when play starts like it is for audio
505 if (PLAY.equals(playStatus) && !CDDA.equals(currentDiscType)) {
506 connector.sendCommand(OppoCommand.QUERY_SUBTITLE_TYPE);
508 currentPlayMode = playStatus;
511 updateChannelState(CHANNEL_REPEAT_MODE, updateData);
514 updateChannelState(CHANNEL_ZOOM_MODE, updateData);
518 // try to normalize the slightly different responses between UDT and QDT
519 final String discType = OppoStatusCodes.DISC_TYPE.get(updateData);
520 currentDiscType = (discType != null ? discType : updateData);
521 updateChannelState(CHANNEL_DISC_TYPE, currentDiscType);
524 // we got the audio type status update, throw it away
525 // and call the query because the text output is better
526 // wait before sending the command to give the player time to catch up
527 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
528 connector.sendCommand(OppoCommand.QUERY_AUDIO_TYPE);
531 updateChannelState(CHANNEL_AUDIO_TYPE, updateData);
534 // we got the subtitle type status update, throw it away
535 // and call the query because the text output is better
536 // wait before sending the command to give the player time to catch up
537 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
538 connector.sendCommand(OppoCommand.QUERY_SUBTITLE_TYPE);
541 updateChannelState(CHANNEL_SUBTITLE_TYPE, updateData);
543 case UAR: // 203 & 205 only
544 updateChannelState(CHANNEL_ASPECT_RATIO, updateData);
547 // example: _480I60 1080P60 - 1st source res, 2nd output res
548 String[] resolution = updateData.replace(UNDERSCORE, BLANK).split(SPACE);
549 if (resolution.length == 2) {
550 updateChannelState(CHANNEL_SOURCE_RESOLUTION, resolution[0]);
551 updateChannelState(CHANNEL_OUTPUT_RESOLUTION, resolution[1]);
555 updateChannelState(CHANNEL_3D_INDICATOR, updateData);
558 updateChannelState(CHANNEL_SUB_SHIFT, updateData);
561 updateChannelState(CHANNEL_OSD_POSITION, updateData);
565 updateChannelState(CHANNEL_HDMI_MODE, updateData);
567 handleHdmiModeUpdate(updateData);
570 case QHR: // 203 & 205 only
571 updateChannelState(CHANNEL_HDR_MODE, updateData);
574 logger.debug("onNewMessageEvent: unhandled key {}, value: {}", key, updateData);
577 } catch (OppoException | InterruptedException e) {
578 logger.debug("Exception processing event from player: {}", e.getMessage());
584 * Schedule the reconnection job
586 private void scheduleReconnectJob() {
587 logger.debug("Schedule reconnect job");
588 cancelReconnectJob();
590 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
591 if (!connector.isConnected()) {
592 logger.debug("Trying to reconnect...");
595 synchronized (sequenceLock) {
596 if (openConnection()) {
598 long prevUpdateTime = lastEventReceived;
600 connector.sendCommand(OppoCommand.QUERY_POWER_STATUS);
601 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
603 // if the player is off most of these won't really do much...
604 OppoCommand.QUERY_COMMANDS.forEach(cmd -> {
606 connector.sendCommand(cmd);
607 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
608 } catch (OppoException | InterruptedException e) {
609 logger.debug("Exception sending initial commands: {}", e.getMessage());
613 // prevUpdateTime should have changed if a message was received from the player
614 if (prevUpdateTime == lastEventReceived) {
615 error = "Player not responding to status requests";
617 } catch (OppoException | InterruptedException e) {
618 error = "First command after connection failed";
619 logger.debug("{}: {}", error, e.getMessage());
622 error = "Reconnection failed";
625 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
628 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
629 isInitialQuery = false;
634 }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
638 * Cancel the reconnection job
640 private void cancelReconnectJob() {
641 ScheduledFuture<?> reconnectJob = this.reconnectJob;
642 if (reconnectJob != null) {
643 reconnectJob.cancel(true);
644 this.reconnectJob = null;
649 * Schedule the polling job
651 private void schedulePollingJob() {
652 logger.debug("Schedule polling job");
655 // when the Oppo is off, this will keep the connection (esp Serial over IP) alive and
656 // detect if the connection goes down
657 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
658 if (connector.isConnected()) {
659 logger.debug("Polling the player for updated status...");
661 synchronized (sequenceLock) {
663 // Verbose mode 2 & 3 only do once until power comes on OR always for BDP direct IP
664 if ((!isPowerOn && !isInitialQuery) || isBdpIP) {
665 connector.sendCommand(OppoCommand.QUERY_POWER_STATUS);
669 // the verbose mode must be set while the player is on
670 if (!isVbModeSet && !isBdpIP) {
671 connector.sendCommand(OppoCommand.SET_VERBOSE_MODE, this.verboseMode);
673 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
676 // Verbose mode 2 & 3 only do once OR always for BDP direct IP
677 if (!isInitialQuery || isBdpIP) {
678 isInitialQuery = true;
679 OppoCommand.QUERY_COMMANDS.forEach(cmd -> {
681 connector.sendCommand(cmd);
682 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
683 } catch (OppoException | InterruptedException e) {
684 logger.debug("Exception sending polling commands: {}", e.getMessage());
689 // for Verbose mode 2 get the current play back time if we are playing, otherwise just do
691 if ((VERBOSE_2.equals(this.verboseMode) && PLAY.equals(currentPlayMode)) || isBdpIP) {
692 switch (currentTimeMode) {
694 connector.sendCommand(OppoCommand.QUERY_TITLE_ELAPSED);
697 connector.sendCommand(OppoCommand.QUERY_TITLE_REMAIN);
700 connector.sendCommand(OppoCommand.QUERY_CHAPTER_ELAPSED);
703 connector.sendCommand(OppoCommand.QUERY_CHAPTER_REMAIN);
706 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
708 // make queries to refresh total number of titles/tracks & chapters
709 connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK);
710 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
711 connector.sendCommand(OppoCommand.QUERY_CHAPTER);
712 } else if (!isBdpIP) {
714 connector.sendCommand(OppoCommand.NO_OP);
718 } catch (OppoException | InterruptedException e) {
719 logger.warn("Polling error: {}", e.getMessage());
722 // if the last event received was more than 1.25 intervals ago,
723 // the player is not responding even though the connection is still good
724 if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_SEC * 1.25 * 1000)) {
725 logger.debug("Player not responding to status requests");
726 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
727 "Player not responding to status requests");
729 scheduleReconnectJob();
733 }, INITIAL_POLLING_DELAY_SEC, POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
737 * Cancel the polling job
739 private void cancelPollingJob() {
740 ScheduledFuture<?> pollingJob = this.pollingJob;
741 if (pollingJob != null) {
742 pollingJob.cancel(true);
743 this.pollingJob = null;
748 * Update the state of a channel
750 * @param channel the channel
751 * @param value the value to be updated
753 private void updateChannelState(String channel, String value) {
754 if (!isLinked(channel)) {
758 if (UNDEF.equals(value)) {
759 updateState(channel, UnDefType.UNDEF);
763 State state = UnDefType.UNDEF;
766 case CHANNEL_TIME_DISPLAY:
767 String[] timeArr = value.split(COLON);
768 if (timeArr.length == 3) {
769 int seconds = (Integer.parseInt(timeArr[0]) * 3600) + (Integer.parseInt(timeArr[1]) * 60)
770 + Integer.parseInt(timeArr[2]);
771 state = new QuantityType<>(seconds, Units.SECOND);
773 state = UnDefType.UNDEF;
778 state = ON.equals(value) ? OnOffType.ON : OnOffType.OFF;
781 case CHANNEL_SUB_SHIFT:
782 case CHANNEL_OSD_POSITION:
783 case CHANNEL_CURRENT_TITLE:
784 case CHANNEL_TOTAL_TITLE:
785 case CHANNEL_CURRENT_CHAPTER:
786 case CHANNEL_TOTAL_CHAPTER:
787 state = new DecimalType(value);
790 state = new PercentType(BigDecimal.valueOf(Integer.parseInt(value)));
792 case CHANNEL_PLAY_MODE:
793 case CHANNEL_TIME_MODE:
794 case CHANNEL_REPEAT_MODE:
795 case CHANNEL_ZOOM_MODE:
796 case CHANNEL_DISC_TYPE:
797 case CHANNEL_AUDIO_TYPE:
798 case CHANNEL_SUBTITLE_TYPE:
799 case CHANNEL_ASPECT_RATIO:
800 case CHANNEL_SOURCE_RESOLUTION:
801 case CHANNEL_OUTPUT_RESOLUTION:
802 case CHANNEL_3D_INDICATOR:
803 case CHANNEL_HDMI_MODE:
804 case CHANNEL_HDR_MODE:
805 state = new StringType(value);
810 updateState(channel, state);
814 * Handle a button press from a UI Player item
816 * @param command the control button press command received
818 private void handleControlCommand(Command command) throws OppoException {
819 if (command instanceof PlayPauseType) {
820 if (command == PlayPauseType.PLAY) {
821 connector.sendCommand(OppoCommand.PLAY);
822 } else if (command == PlayPauseType.PAUSE) {
823 connector.sendCommand(OppoCommand.PAUSE);
825 } else if (command instanceof NextPreviousType) {
826 if (command == NextPreviousType.NEXT) {
827 connector.sendCommand(OppoCommand.NEXT);
828 } else if (command == NextPreviousType.PREVIOUS) {
829 connector.sendCommand(OppoCommand.PREV);
831 } else if (command instanceof RewindFastforwardType) {
832 if (command == RewindFastforwardType.FASTFORWARD) {
833 connector.sendCommand(OppoCommand.FFORWARD);
834 } else if (command == RewindFastforwardType.REWIND) {
835 connector.sendCommand(OppoCommand.REWIND);
838 logger.warn("Unknown control command: {}", command);
842 private void buildOptionDropdowns(int model) {
843 if (model == MODEL83 || model == MODEL103 || model == MODEL105) {
844 hdmiModeOptions.add(new StateOption("AUTO", "Auto"));
845 hdmiModeOptions.add(new StateOption("SRC", "Source Direct"));
846 if (model != MODEL83) {
847 hdmiModeOptions.add(new StateOption("4K2K", "4K*2K"));
849 hdmiModeOptions.add(new StateOption("1080P", "1080P"));
850 hdmiModeOptions.add(new StateOption("1080I", "1080I"));
851 hdmiModeOptions.add(new StateOption("720P", "720P"));
852 hdmiModeOptions.add(new StateOption("SDP", "480P"));
853 hdmiModeOptions.add(new StateOption("SDI", "480I"));
856 if (model == MODEL103 || model == MODEL105) {
857 inputSourceOptions.add(new StateOption("0", "Blu-Ray Player"));
858 inputSourceOptions.add(new StateOption("1", "HDMI/MHL IN-Front"));
859 inputSourceOptions.add(new StateOption("2", "HDMI IN-Back"));
860 inputSourceOptions.add(new StateOption("3", "ARC"));
862 if (model == MODEL105) {
863 inputSourceOptions.add(new StateOption("4", "Optical In"));
864 inputSourceOptions.add(new StateOption("5", "Coaxial In"));
865 inputSourceOptions.add(new StateOption("6", "USB Audio In"));
869 if (model == MODEL203 || model == MODEL205) {
870 hdmiModeOptions.add(new StateOption("AUTO", "Auto"));
871 hdmiModeOptions.add(new StateOption("SRC", "Source Direct"));
872 hdmiModeOptions.add(new StateOption("UHD_AUTO", "UHD Auto"));
873 hdmiModeOptions.add(new StateOption("UHD24", "UHD24"));
874 hdmiModeOptions.add(new StateOption("UHD50", "UHD50"));
875 hdmiModeOptions.add(new StateOption("UHD60", "UHD60"));
876 hdmiModeOptions.add(new StateOption("1080P_AUTO", "1080P Auto"));
877 hdmiModeOptions.add(new StateOption("1080P24", "1080P24"));
878 hdmiModeOptions.add(new StateOption("1080P50", "1080P50"));
879 hdmiModeOptions.add(new StateOption("1080P60", "1080P60"));
880 hdmiModeOptions.add(new StateOption("1080I50", "1080I50"));
881 hdmiModeOptions.add(new StateOption("1080I60", "1080I60"));
882 hdmiModeOptions.add(new StateOption("720P50", "720P50"));
883 hdmiModeOptions.add(new StateOption("720P60", "720P60"));
884 hdmiModeOptions.add(new StateOption("576P", "567P"));
885 hdmiModeOptions.add(new StateOption("576I", "567I"));
886 hdmiModeOptions.add(new StateOption("480P", "480P"));
887 hdmiModeOptions.add(new StateOption("480I", "480I"));
889 inputSourceOptions.add(new StateOption("0", "Blu-Ray Player"));
890 inputSourceOptions.add(new StateOption("1", "HDMI IN"));
891 inputSourceOptions.add(new StateOption("2", "ARC"));
893 if (model == MODEL205) {
894 inputSourceOptions.add(new StateOption("3", "Optical In"));
895 inputSourceOptions.add(new StateOption("4", "Coaxial In"));
896 inputSourceOptions.add(new StateOption("5", "USB Audio In"));
901 private void handleHdmiModeUpdate(String updateData) {
902 // ugly... a couple of the query hdmi mode response codes on the earlier models don't match the code to set it
903 // some of this protocol is weird like that...
904 if ("480I".equals(updateData)) {
905 updateChannelState(CHANNEL_HDMI_MODE, "SDI");
906 } else if ("480P".equals(updateData)) {
907 updateChannelState(CHANNEL_HDMI_MODE, "SDP");
908 } else if ("4K*2K".equals(updateData)) {
909 updateChannelState(CHANNEL_HDMI_MODE, "4K2K");
911 updateChannelState(CHANNEL_HDMI_MODE, updateData);