]> git.basschouten.com Git - openhab-addons.git/blob
89cbfbb0cc8ab32dc72b73e532163a14b3b3816c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.oppo.internal.handler;
14
15 import static org.openhab.binding.oppo.internal.OppoBindingConstants.*;
16 import static org.openhab.core.thing.Thing.*;
17
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;
25
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;
61
62 /**
63  * The {@link OppoHandler} is responsible for handling commands, which are sent to one of the channels.
64  *
65  * Based on the Rotel binding by Laurent Garnier
66  *
67  * @author Michael Lobstein - Initial contribution
68  */
69 @NonNullByDefault
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;
75
76     private static final Pattern TIME_CODE_PATTERN = Pattern
77             .compile("^(\\d{3}) (\\d{3}) ([A-Z]{1}) (\\d{2}:\\d{2}:\\d{2})$");
78
79     private final Logger logger = LoggerFactory.getLogger(OppoHandler.class);
80
81     private @Nullable ScheduledFuture<?> reconnectJob;
82     private @Nullable ScheduledFuture<?> pollingJob;
83
84     private OppoStateDescriptionOptionProvider stateDescriptionProvider;
85     private SerialPortManager serialPortManager;
86     private OppoConnector connector = new OppoDefaultConnector();
87
88     private List<StateOption> inputSourceOptions = new ArrayList<>();
89     private List<StateOption> hdmiModeOptions = new ArrayList<>();
90
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();
103
104     /**
105      * Constructor
106      */
107     public OppoHandler(Thing thing, OppoStateDescriptionOptionProvider stateDescriptionProvider,
108             SerialPortManager serialPortManager) {
109         super(thing);
110         this.stateDescriptionProvider = stateDescriptionProvider;
111         this.serialPortManager = serialPortManager;
112     }
113
114     @Override
115     public void initialize() {
116         OppoThingConfiguration config = getConfigAs(OppoThingConfiguration.class);
117         final String uid = this.getThing().getUID().getAsString();
118
119         // Check configuration settings
120         String configError = null;
121         boolean override = false;
122
123         Integer model = config.model;
124         String serialPort = config.serialPort;
125         String host = config.host;
126         Integer port = config.port;
127
128         if (model == null) {
129             configError = "player model must be specified";
130             return;
131         }
132
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";
138             }
139         } else {
140             if (port == null) {
141                 if (model == MODEL83) {
142                     port = BDP83_PORT;
143                     override = true;
144                     this.isBdpIP = true;
145                 } else if (model == MODEL103 || model == MODEL105) {
146                     port = BDP10X_PORT;
147                     override = true;
148                     this.isBdpIP = true;
149                 } else {
150                     port = BDP20X_PORT;
151                 }
152             } else if (port <= 0) {
153                 configError = "invalid port configuration setting";
154             }
155         }
156
157         if (configError != null) {
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
159             return;
160         }
161
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);
167         } else {
168             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
169                     "Either Serial port or Host & Port must be specifed");
170             return;
171         }
172
173         if (config.verboseMode) {
174             this.verboseMode = VERBOSE_3;
175         }
176
177         if (model == MODEL203 || model == MODEL205) {
178             this.isUDP20X = true;
179         }
180
181         this.buildOptionDropdowns(model);
182         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SOURCE),
183                 inputSourceOptions);
184         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_HDMI_MODE),
185                 hdmiModeOptions);
186
187         // remove channels not needed for this model
188         List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
189
190         if (model == MODEL83) {
191             channels.removeIf(c -> (c.getUID().getId().equals(CHANNEL_SUB_SHIFT)
192                     || c.getUID().getId().equals(CHANNEL_OSD_POSITION)));
193         }
194
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)));
198         }
199
200         // no query to determine this, so set the default value at startup
201         updateChannelState(CHANNEL_TIME_MODE, currentTimeMode);
202
203         updateThing(editThing().withChannels(channels).build());
204
205         scheduleReconnectJob();
206         schedulePollingJob();
207
208         updateStatus(ThingStatus.UNKNOWN);
209     }
210
211     @Override
212     public void dispose() {
213         cancelReconnectJob();
214         cancelPollingJob();
215         closeConnection();
216         super.dispose();
217     }
218
219     /**
220      * Handle a command the UI
221      *
222      * @param channelUID the channel sending the command
223      * @param command the command received
224      *
225      */
226     @Override
227     public void handleCommand(ChannelUID channelUID, Command command) {
228         String channel = channelUID.getId();
229
230         if (getThing().getStatus() != ThingStatus.ONLINE) {
231             logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
232             return;
233         }
234
235         if (!connector.isConnected()) {
236             logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
237             return;
238         }
239
240         synchronized (sequenceLock) {
241             try {
242                 String commandStr = command.toString();
243                 switch (channel) {
244                     case CHANNEL_POWER:
245                         if (command instanceof OnOffType) {
246                             connector.sendCommand(
247                                     command == OnOffType.ON ? OppoCommand.POWER_ON : OppoCommand.POWER_OFF);
248
249                             // set the power flag to false only, will be set true by QPW or UPW messages
250                             if (command == OnOffType.OFF) {
251                                 isPowerOn = false;
252                                 isInitialQuery = false;
253                             }
254                         }
255                         break;
256                     case CHANNEL_VOLUME:
257                         if (command instanceof PercentType) {
258                             connector.sendCommand(OppoCommand.SET_VOLUME_LEVEL, commandStr);
259                         }
260                         break;
261                     case CHANNEL_MUTE:
262                         if (command instanceof OnOffType) {
263                             if (command == OnOffType.ON) {
264                                 connector.sendCommand(OppoCommand.SET_VOLUME_LEVEL, MUTE);
265                             } else {
266                                 connector.sendCommand(OppoCommand.MUTE);
267                             }
268                         }
269                         break;
270                     case CHANNEL_SOURCE:
271                         if (command instanceof DecimalType decimalCommand) {
272                             int value = decimalCommand.intValue();
273                             connector.sendCommand(OppoCommand.SET_INPUT_SOURCE, String.valueOf(value));
274                         }
275                         break;
276                     case CHANNEL_CONTROL:
277                         this.handleControlCommand(command);
278                         break;
279                     case CHANNEL_TIME_MODE:
280                         if (command instanceof StringType) {
281                             connector.sendCommand(OppoCommand.SET_TIME_DISPLAY, commandStr);
282                             currentTimeMode = commandStr;
283                         }
284                         break;
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));
290                         }
291                         break;
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));
297                         }
298                         break;
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));
303                         }
304                         break;
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));
309                         }
310                         break;
311                     case CHANNEL_HDMI_MODE:
312                         if (command instanceof StringType) {
313                             connector.sendCommand(OppoCommand.SET_HDMI_MODE, commandStr);
314                         }
315                         break;
316                     case CHANNEL_HDR_MODE:
317                         if (command instanceof StringType) {
318                             connector.sendCommand(OppoCommand.SET_HDR_MODE, commandStr);
319                         }
320                         break;
321                     case CHANNEL_REMOTE_BUTTON:
322                         if (command instanceof StringType) {
323                             connector.sendCommand(commandStr);
324                         }
325                         break;
326                     default:
327                         logger.debug("Unknown command {} from channel {}", command, channel);
328                         break;
329                 }
330             } catch (OppoException e) {
331                 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
332                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
333                 closeConnection();
334                 scheduleReconnectJob();
335             }
336         }
337     }
338
339     /**
340      * Open the connection with the Oppo player
341      *
342      * @return true if the connection is opened successfully or false if not
343      */
344     private synchronized boolean openConnection() {
345         connector.addEventListener(this);
346         try {
347             connector.open();
348         } catch (OppoException e) {
349             logger.debug("openConnection() failed: {}", e.getMessage());
350         }
351         logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
352         return connector.isConnected();
353     }
354
355     /**
356      * Close the connection with the Oppo player
357      */
358     private synchronized void closeConnection() {
359         if (connector.isConnected()) {
360             connector.close();
361             connector.removeEventListener(this);
362             logger.debug("closeConnection(): disconnected");
363         }
364     }
365
366     /**
367      * Handle an event received from the Oppo player
368      *
369      * @param evt the event to process
370      */
371     @Override
372     public void onNewMessageEvent(OppoMessageEvent evt) {
373         logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
374         lastEventReceived = System.currentTimeMillis();
375
376         String key = evt.getKey();
377         String updateData = evt.getValue().trim();
378         if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
379             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
380         }
381
382         synchronized (sequenceLock) {
383             try {
384                 switch (key) {
385                     case NOP: // ignore
386                         break;
387                     case UTC:
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,
390                         // g4 = time
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);
401                             }
402
403                             if (!currentTimeMode.equals(matcher.group(3))) {
404                                 currentTimeMode = matcher.group(3);
405                                 updateChannelState(CHANNEL_TIME_MODE, currentTimeMode);
406                             }
407                             updateChannelState(CHANNEL_TIME_DISPLAY, matcher.group(4));
408                         } else {
409                             logger.debug("no match on message: {}", updateData);
410                         }
411                         break;
412                     case QTE:
413                     case QTR:
414                     case QCE:
415                     case QCR:
416                         // these are used with verbose mode 2
417                         updateChannelState(CHANNEL_TIME_DISPLAY, updateData);
418                         break;
419                     case QVR:
420                         thing.setProperty(PROPERTY_FIRMWARE_VERSION, updateData);
421                         break;
422                     case QPW:
423                         updateChannelState(CHANNEL_POWER, updateData);
424                         if (OFF.equals(updateData)) {
425                             currentPlayMode = BLANK;
426                             isPowerOn = false;
427                         } else {
428                             isPowerOn = true;
429                         }
430                         break;
431                     case UPW:
432                         updateChannelState(CHANNEL_POWER, ONE.equals(updateData) ? ON : OFF);
433                         if (ZERO.equals(updateData)) {
434                             currentPlayMode = BLANK;
435                             isPowerOn = false;
436                             isInitialQuery = false;
437                         } else {
438                             isPowerOn = true;
439                         }
440                         break;
441                     case QVL:
442                     case UVL:
443                     case VUP:
444                     case VDN:
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);
449                         } else {
450                             updateChannelState(CHANNEL_VOLUME, updateData);
451                             updateChannelState(CHANNEL_MUTE, OFF);
452                         }
453                         break;
454                     case QIS:
455                     case UIS:
456                         // example: 0 BD-PLAYER, split off just the number
457                         updateChannelState(CHANNEL_SOURCE, updateData.split(SPACE)[0]);
458                         break;
459                     case QTK:
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]);
465                         }
466                         break;
467                     case QCH:
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]);
473                         }
474                         break;
475                     case UPL:
476                     case QPL:
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;
481                         }
482
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);
493                         }
494                         updateChannelState(CHANNEL_PLAY_MODE, playStatus);
495                         updateState(CHANNEL_CONTROL,
496                                 PLAY.equals(playStatus) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
497
498                         // ejecting the disc does not produce a UDT message, so clear disc type manually
499                         if (OPEN.equals(playStatus) || NO_DISC.equals(playStatus)) {
500                             updateChannelState(CHANNEL_DISC_TYPE, UNKNOW_DISC);
501                             currentDiscType = BLANK;
502                         }
503
504                         // if switching to play mode and not a CD then query the subtitle type...
505                         // because if subtitles were on when playback stopped, they got nulled out above
506                         // and the subtitle update message ("UST") is not sent when play starts like it is for audio
507                         if (PLAY.equals(playStatus) && !CDDA.equals(currentDiscType)) {
508                             connector.sendCommand(OppoCommand.QUERY_SUBTITLE_TYPE);
509                         }
510                         currentPlayMode = playStatus;
511                         break;
512                     case QRP:
513                         updateChannelState(CHANNEL_REPEAT_MODE, updateData);
514                         break;
515                     case QZM:
516                         updateChannelState(CHANNEL_ZOOM_MODE, updateData);
517                         break;
518                     case UDT:
519                     case QDT:
520                         // try to normalize the slightly different responses between UDT and QDT
521                         final String discType = OppoStatusCodes.DISC_TYPE.get(updateData);
522                         currentDiscType = (discType != null ? discType : updateData);
523                         updateChannelState(CHANNEL_DISC_TYPE, currentDiscType);
524                         break;
525                     case UAT:
526                         // we got the audio type status update, throw it away
527                         // and call the query because the text output is better
528                         // wait before sending the command to give the player time to catch up
529                         Thread.sleep(SLEEP_BETWEEN_CMD_MS);
530                         connector.sendCommand(OppoCommand.QUERY_AUDIO_TYPE);
531                         break;
532                     case QAT:
533                         updateChannelState(CHANNEL_AUDIO_TYPE, updateData);
534                         break;
535                     case UST:
536                         // we got the subtitle type status update, throw it away
537                         // and call the query because the text output is better
538                         // wait before sending the command to give the player time to catch up
539                         Thread.sleep(SLEEP_BETWEEN_CMD_MS);
540                         connector.sendCommand(OppoCommand.QUERY_SUBTITLE_TYPE);
541                         break;
542                     case QST:
543                         updateChannelState(CHANNEL_SUBTITLE_TYPE, updateData);
544                         break;
545                     case UAR: // 203 & 205 only
546                         updateChannelState(CHANNEL_ASPECT_RATIO, updateData);
547                         break;
548                     case UVO:
549                         // example: _480I60 1080P60 - 1st source res, 2nd output res
550                         String[] resolution = updateData.replace(UNDERSCORE, BLANK).split(SPACE);
551                         if (resolution.length == 2) {
552                             updateChannelState(CHANNEL_SOURCE_RESOLUTION, resolution[0]);
553                             updateChannelState(CHANNEL_OUTPUT_RESOLUTION, resolution[1]);
554                         }
555                         break;
556                     case U3D:
557                         updateChannelState(CHANNEL_3D_INDICATOR, updateData);
558                         break;
559                     case QSH:
560                         updateChannelState(CHANNEL_SUB_SHIFT, updateData);
561                         break;
562                     case QOP:
563                         updateChannelState(CHANNEL_OSD_POSITION, updateData);
564                         break;
565                     case QHD:
566                         if (this.isUDP20X) {
567                             updateChannelState(CHANNEL_HDMI_MODE, updateData);
568                         } else {
569                             handleHdmiModeUpdate(updateData);
570                         }
571                         break;
572                     case QHR: // 203 & 205 only
573                         updateChannelState(CHANNEL_HDR_MODE, updateData);
574                         break;
575                     default:
576                         logger.debug("onNewMessageEvent: unhandled key {}, value: {}", key, updateData);
577                         break;
578                 }
579             } catch (OppoException | InterruptedException e) {
580                 logger.debug("Exception processing event from player: {}", e.getMessage());
581             }
582         }
583     }
584
585     /**
586      * Schedule the reconnection job
587      */
588     private void scheduleReconnectJob() {
589         logger.debug("Schedule reconnect job");
590         cancelReconnectJob();
591
592         reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
593             if (!connector.isConnected()) {
594                 logger.debug("Trying to reconnect...");
595                 closeConnection();
596                 String error = null;
597                 synchronized (sequenceLock) {
598                     if (openConnection()) {
599                         try {
600                             long prevUpdateTime = lastEventReceived;
601
602                             connector.sendCommand(OppoCommand.QUERY_POWER_STATUS);
603                             Thread.sleep(SLEEP_BETWEEN_CMD_MS);
604
605                             // if the player is off most of these won't really do much...
606                             OppoCommand.QUERY_COMMANDS.forEach(cmd -> {
607                                 try {
608                                     connector.sendCommand(cmd);
609                                     Thread.sleep(SLEEP_BETWEEN_CMD_MS);
610                                 } catch (OppoException | InterruptedException e) {
611                                     logger.debug("Exception sending initial commands: {}", e.getMessage());
612                                 }
613                             });
614
615                             // prevUpdateTime should have changed if a message was received from the player
616                             if (prevUpdateTime == lastEventReceived) {
617                                 error = "Player not responding to status requests";
618                             }
619                         } catch (OppoException | InterruptedException e) {
620                             error = "First command after connection failed";
621                             logger.debug("{}: {}", error, e.getMessage());
622                         }
623                     } else {
624                         error = "Reconnection failed";
625                     }
626                     if (error != null) {
627                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
628                         closeConnection();
629                     } else {
630                         updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
631                         isInitialQuery = false;
632                         isVbModeSet = false;
633                     }
634                 }
635             }
636         }, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
637     }
638
639     /**
640      * Cancel the reconnection job
641      */
642     private void cancelReconnectJob() {
643         ScheduledFuture<?> reconnectJob = this.reconnectJob;
644         if (reconnectJob != null) {
645             reconnectJob.cancel(true);
646             this.reconnectJob = null;
647         }
648     }
649
650     /**
651      * Schedule the polling job
652      */
653     private void schedulePollingJob() {
654         logger.debug("Schedule polling job");
655         cancelPollingJob();
656
657         // when the Oppo is off, this will keep the connection (esp Serial over IP) alive and
658         // detect if the connection goes down
659         pollingJob = scheduler.scheduleWithFixedDelay(() -> {
660             if (connector.isConnected()) {
661                 logger.debug("Polling the player for updated status...");
662
663                 synchronized (sequenceLock) {
664                     try {
665                         // Verbose mode 2 & 3 only do once until power comes on OR always for BDP direct IP
666                         if ((!isPowerOn && !isInitialQuery) || isBdpIP) {
667                             connector.sendCommand(OppoCommand.QUERY_POWER_STATUS);
668                         }
669
670                         if (isPowerOn) {
671                             // the verbose mode must be set while the player is on
672                             if (!isVbModeSet && !isBdpIP) {
673                                 connector.sendCommand(OppoCommand.SET_VERBOSE_MODE, this.verboseMode);
674                                 isVbModeSet = true;
675                                 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
676                             }
677
678                             // Verbose mode 2 & 3 only do once OR always for BDP direct IP
679                             if (!isInitialQuery || isBdpIP) {
680                                 isInitialQuery = true;
681                                 OppoCommand.QUERY_COMMANDS.forEach(cmd -> {
682                                     try {
683                                         connector.sendCommand(cmd);
684                                         Thread.sleep(SLEEP_BETWEEN_CMD_MS);
685                                     } catch (OppoException | InterruptedException e) {
686                                         logger.debug("Exception sending polling commands: {}", e.getMessage());
687                                     }
688                                 });
689                             }
690
691                             // for Verbose mode 2 get the current play back time if we are playing, otherwise just do
692                             // NO_OP
693                             if ((VERBOSE_2.equals(this.verboseMode) && PLAY.equals(currentPlayMode)) || isBdpIP) {
694                                 switch (currentTimeMode) {
695                                     case T:
696                                         connector.sendCommand(OppoCommand.QUERY_TITLE_ELAPSED);
697                                         break;
698                                     case X:
699                                         connector.sendCommand(OppoCommand.QUERY_TITLE_REMAIN);
700                                         break;
701                                     case C:
702                                         connector.sendCommand(OppoCommand.QUERY_CHAPTER_ELAPSED);
703                                         break;
704                                     case K:
705                                         connector.sendCommand(OppoCommand.QUERY_CHAPTER_REMAIN);
706                                         break;
707                                 }
708                                 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
709
710                                 // make queries to refresh total number of titles/tracks & chapters
711                                 connector.sendCommand(OppoCommand.QUERY_TITLE_TRACK);
712                                 Thread.sleep(SLEEP_BETWEEN_CMD_MS);
713                                 connector.sendCommand(OppoCommand.QUERY_CHAPTER);
714                             } else if (!isBdpIP) {
715                                 // verbose mode 3
716                                 connector.sendCommand(OppoCommand.NO_OP);
717                             }
718                         }
719
720                     } catch (OppoException | InterruptedException e) {
721                         logger.debug("Polling error: {}", e.getMessage());
722                     }
723
724                     // if the last event received was more than 1.25 intervals ago,
725                     // the player is not responding even though the connection is still good
726                     if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_SEC * 1.25 * 1000)) {
727                         logger.debug("Player not responding to status requests");
728                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
729                                 "Player not responding to status requests");
730                         closeConnection();
731                         scheduleReconnectJob();
732                     }
733                 }
734             }
735         }, INITIAL_POLLING_DELAY_SEC, POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
736     }
737
738     /**
739      * Cancel the polling job
740      */
741     private void cancelPollingJob() {
742         ScheduledFuture<?> pollingJob = this.pollingJob;
743         if (pollingJob != null) {
744             pollingJob.cancel(true);
745             this.pollingJob = null;
746         }
747     }
748
749     /**
750      * Update the state of a channel
751      *
752      * @param channel the channel
753      * @param value the value to be updated
754      */
755     private void updateChannelState(String channel, String value) {
756         if (!isLinked(channel)) {
757             return;
758         }
759
760         if (UNDEF.equals(value)) {
761             updateState(channel, UnDefType.UNDEF);
762             return;
763         }
764
765         State state = UnDefType.UNDEF;
766
767         switch (channel) {
768             case CHANNEL_TIME_DISPLAY:
769                 String[] timeArr = value.split(COLON);
770                 if (timeArr.length == 3) {
771                     int seconds = (Integer.parseInt(timeArr[0]) * 3600) + (Integer.parseInt(timeArr[1]) * 60)
772                             + Integer.parseInt(timeArr[2]);
773                     state = new QuantityType<>(seconds, Units.SECOND);
774                 } else {
775                     state = UnDefType.UNDEF;
776                 }
777                 break;
778             case CHANNEL_POWER:
779             case CHANNEL_MUTE:
780                 state = OnOffType.from(ON.equals(value));
781                 break;
782             case CHANNEL_SOURCE:
783             case CHANNEL_SUB_SHIFT:
784             case CHANNEL_OSD_POSITION:
785             case CHANNEL_CURRENT_TITLE:
786             case CHANNEL_TOTAL_TITLE:
787             case CHANNEL_CURRENT_CHAPTER:
788             case CHANNEL_TOTAL_CHAPTER:
789                 state = new DecimalType(value);
790                 break;
791             case CHANNEL_VOLUME:
792                 state = new PercentType(BigDecimal.valueOf(Integer.parseInt(value)));
793                 break;
794             case CHANNEL_PLAY_MODE:
795             case CHANNEL_TIME_MODE:
796             case CHANNEL_REPEAT_MODE:
797             case CHANNEL_ZOOM_MODE:
798             case CHANNEL_DISC_TYPE:
799             case CHANNEL_AUDIO_TYPE:
800             case CHANNEL_SUBTITLE_TYPE:
801             case CHANNEL_ASPECT_RATIO:
802             case CHANNEL_SOURCE_RESOLUTION:
803             case CHANNEL_OUTPUT_RESOLUTION:
804             case CHANNEL_3D_INDICATOR:
805             case CHANNEL_HDMI_MODE:
806             case CHANNEL_HDR_MODE:
807                 state = new StringType(value);
808                 break;
809             default:
810                 break;
811         }
812         updateState(channel, state);
813     }
814
815     /**
816      * Handle a button press from a UI Player item
817      *
818      * @param command the control button press command received
819      */
820     private void handleControlCommand(Command command) throws OppoException {
821         if (command instanceof PlayPauseType) {
822             if (command == PlayPauseType.PLAY) {
823                 connector.sendCommand(OppoCommand.PLAY);
824             } else if (command == PlayPauseType.PAUSE) {
825                 connector.sendCommand(OppoCommand.PAUSE);
826             }
827         } else if (command instanceof NextPreviousType) {
828             if (command == NextPreviousType.NEXT) {
829                 connector.sendCommand(OppoCommand.NEXT);
830             } else if (command == NextPreviousType.PREVIOUS) {
831                 connector.sendCommand(OppoCommand.PREV);
832             }
833         } else if (command instanceof RewindFastforwardType) {
834             if (command == RewindFastforwardType.FASTFORWARD) {
835                 connector.sendCommand(OppoCommand.FFORWARD);
836             } else if (command == RewindFastforwardType.REWIND) {
837                 connector.sendCommand(OppoCommand.REWIND);
838             }
839         } else {
840             logger.debug("Unknown control command: {}", command);
841         }
842     }
843
844     private void buildOptionDropdowns(int model) {
845         if (model == MODEL83 || model == MODEL103 || model == MODEL105) {
846             hdmiModeOptions.add(new StateOption("AUTO", "Auto"));
847             hdmiModeOptions.add(new StateOption("SRC", "Source Direct"));
848             if (model != MODEL83) {
849                 hdmiModeOptions.add(new StateOption("4K2K", "4K*2K"));
850             }
851             hdmiModeOptions.add(new StateOption("1080P", "1080P"));
852             hdmiModeOptions.add(new StateOption("1080I", "1080I"));
853             hdmiModeOptions.add(new StateOption("720P", "720P"));
854             hdmiModeOptions.add(new StateOption("SDP", "480P"));
855             hdmiModeOptions.add(new StateOption("SDI", "480I"));
856         }
857
858         if (model == MODEL103 || model == MODEL105) {
859             inputSourceOptions.add(new StateOption("0", "Blu-Ray Player"));
860             inputSourceOptions.add(new StateOption("1", "HDMI/MHL IN-Front"));
861             inputSourceOptions.add(new StateOption("2", "HDMI IN-Back"));
862             inputSourceOptions.add(new StateOption("3", "ARC"));
863
864             if (model == MODEL105) {
865                 inputSourceOptions.add(new StateOption("4", "Optical In"));
866                 inputSourceOptions.add(new StateOption("5", "Coaxial In"));
867                 inputSourceOptions.add(new StateOption("6", "USB Audio In"));
868             }
869         }
870
871         if (model == MODEL203 || model == MODEL205) {
872             hdmiModeOptions.add(new StateOption("AUTO", "Auto"));
873             hdmiModeOptions.add(new StateOption("SRC", "Source Direct"));
874             hdmiModeOptions.add(new StateOption("UHD_AUTO", "UHD Auto"));
875             hdmiModeOptions.add(new StateOption("UHD24", "UHD24"));
876             hdmiModeOptions.add(new StateOption("UHD50", "UHD50"));
877             hdmiModeOptions.add(new StateOption("UHD60", "UHD60"));
878             hdmiModeOptions.add(new StateOption("1080P_AUTO", "1080P Auto"));
879             hdmiModeOptions.add(new StateOption("1080P24", "1080P24"));
880             hdmiModeOptions.add(new StateOption("1080P50", "1080P50"));
881             hdmiModeOptions.add(new StateOption("1080P60", "1080P60"));
882             hdmiModeOptions.add(new StateOption("1080I50", "1080I50"));
883             hdmiModeOptions.add(new StateOption("1080I60", "1080I60"));
884             hdmiModeOptions.add(new StateOption("720P50", "720P50"));
885             hdmiModeOptions.add(new StateOption("720P60", "720P60"));
886             hdmiModeOptions.add(new StateOption("576P", "567P"));
887             hdmiModeOptions.add(new StateOption("576I", "567I"));
888             hdmiModeOptions.add(new StateOption("480P", "480P"));
889             hdmiModeOptions.add(new StateOption("480I", "480I"));
890
891             inputSourceOptions.add(new StateOption("0", "Blu-Ray Player"));
892             inputSourceOptions.add(new StateOption("1", "HDMI IN"));
893             inputSourceOptions.add(new StateOption("2", "ARC"));
894
895             if (model == MODEL205) {
896                 inputSourceOptions.add(new StateOption("3", "Optical In"));
897                 inputSourceOptions.add(new StateOption("4", "Coaxial In"));
898                 inputSourceOptions.add(new StateOption("5", "USB Audio In"));
899             }
900         }
901     }
902
903     private void handleHdmiModeUpdate(String updateData) {
904         // ugly... a couple of the query hdmi mode response codes on the earlier models don't match the code to set it
905         // some of this protocol is weird like that...
906         if ("480I".equals(updateData)) {
907             updateChannelState(CHANNEL_HDMI_MODE, "SDI");
908         } else if ("480P".equals(updateData)) {
909             updateChannelState(CHANNEL_HDMI_MODE, "SDP");
910         } else if ("4K*2K".equals(updateData)) {
911             updateChannelState(CHANNEL_HDMI_MODE, "4K2K");
912         } else {
913             updateChannelState(CHANNEL_HDMI_MODE, updateData);
914         }
915     }
916 }