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