]> git.basschouten.com Git - openhab-addons.git/blob
31e323d814eb0c93949c5025e4a79e9291820851
[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.kaleidescape.internal.handler;
14
15 import static org.openhab.binding.kaleidescape.internal.KaleidescapeBindingConstants.*;
16
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import javax.measure.Unit;
28 import javax.measure.quantity.Time;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.openhab.binding.kaleidescape.internal.KaleidescapeException;
34 import org.openhab.binding.kaleidescape.internal.KaleidescapeThingActions;
35 import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeConnector;
36 import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeDefaultConnector;
37 import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeIpConnector;
38 import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeMessageEvent;
39 import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeMessageEventListener;
40 import org.openhab.binding.kaleidescape.internal.communication.KaleidescapeSerialConnector;
41 import org.openhab.binding.kaleidescape.internal.configuration.KaleidescapeThingConfiguration;
42 import org.openhab.core.io.transport.serial.SerialPortManager;
43 import org.openhab.core.library.types.NextPreviousType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.PercentType;
46 import org.openhab.core.library.types.PlayPauseType;
47 import org.openhab.core.library.types.RewindFastforwardType;
48 import org.openhab.core.library.types.StringType;
49 import org.openhab.core.library.unit.Units;
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.ThingTypeUID;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.State;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63 /**
64  * The {@link KaleidescapeHandler} is responsible for handling commands, which are sent to one of the channels.
65  *
66  * Based on the Rotel binding by Laurent Garnier
67  *
68  * @author Michael Lobstein - Initial contribution
69  */
70 @NonNullByDefault
71 public class KaleidescapeHandler extends BaseThingHandler implements KaleidescapeMessageEventListener {
72     private static final long RECON_POLLING_INTERVAL_S = 60;
73     private static final long POLLING_INTERVAL_S = 20;
74
75     private final Logger logger = LoggerFactory.getLogger(KaleidescapeHandler.class);
76     private final SerialPortManager serialPortManager;
77     private final Map<String, String> cache = new HashMap<String, String>();
78
79     protected final HttpClient httpClient;
80     protected final Unit<Time> apiSecondUnit = Units.SECOND;
81
82     private ThingTypeUID thingTypeUID = THING_TYPE_PLAYER;
83     private @Nullable ScheduledFuture<?> reconnectJob;
84     private @Nullable ScheduledFuture<?> pollingJob;
85     private long lastEventReceived = 0;
86     private int updatePeriod = 0;
87
88     protected KaleidescapeConnector connector = new KaleidescapeDefaultConnector();
89     protected int metaRuntimeMultiple = 1;
90     protected int volume = 0;
91     protected boolean volumeEnabled = false;
92     protected boolean isMuted = false;
93     protected String friendlyName = EMPTY;
94     protected Object sequenceLock = new Object();
95
96     public KaleidescapeHandler(Thing thing, SerialPortManager serialPortManager, HttpClient httpClient) {
97         super(thing);
98         this.serialPortManager = serialPortManager;
99         this.httpClient = httpClient;
100     }
101
102     protected void updateChannel(String channelUID, State state) {
103         this.updateState(channelUID, state);
104     }
105
106     protected void updateDetailChannel(String channelUID, State state) {
107         this.updateState(DETAIL + channelUID, state);
108     }
109
110     protected void updateThingProperty(String name, String value) {
111         thing.setProperty(name, value);
112     }
113
114     @Override
115     public void initialize() {
116         final String uid = this.getThing().getUID().getAsString();
117         KaleidescapeThingConfiguration config = getConfigAs(KaleidescapeThingConfiguration.class);
118
119         this.thingTypeUID = thing.getThingTypeUID();
120
121         // Check configuration settings
122         String configError = null;
123         final String serialPort = config.serialPort;
124         final String host = config.host;
125         final Integer port = config.port;
126         final Integer updatePeriod = config.updatePeriod;
127
128         if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
129             configError = "undefined serialPort and host configuration settings; please set one of them";
130         } else if (host == null || host.isEmpty()) {
131             if (serialPort != null && serialPort.toLowerCase().startsWith("rfc2217")) {
132                 configError = "use host and port configuration settings for a serial over IP connection";
133             }
134         } else {
135             if (port == null) {
136                 configError = "undefined port configuration setting";
137             } else if (port <= 0) {
138                 configError = "invalid port configuration setting";
139             }
140         }
141
142         if (configError != null) {
143             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
144             return;
145         }
146
147         if (updatePeriod != null) {
148             this.updatePeriod = updatePeriod;
149         }
150
151         // check if volume is enabled
152         if (config.volumeEnabled) {
153             this.volumeEnabled = true;
154             this.volume = config.initialVolume;
155             this.updateState(VOLUME, new PercentType(this.volume));
156             this.updateState(MUTE, OnOffType.OFF);
157         }
158
159         if (serialPort != null) {
160             connector = new KaleidescapeSerialConnector(serialPortManager, serialPort, uid);
161         } else if (port != null) {
162             connector = new KaleidescapeIpConnector(host, port, uid);
163         } else {
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
165                     "Either Serial port or Host & Port must be specifed");
166             return;
167         }
168
169         scheduleReconnectJob();
170         schedulePollingJob();
171
172         updateStatus(ThingStatus.UNKNOWN);
173     }
174
175     @Override
176     public void dispose() {
177         cancelReconnectJob();
178         cancelPollingJob();
179         closeConnection();
180     }
181
182     @Override
183     public Collection<Class<? extends ThingHandlerService>> getServices() {
184         return Collections.singletonList(KaleidescapeThingActions.class);
185     }
186
187     public void handleRawCommand(@Nullable String command) {
188         synchronized (sequenceLock) {
189             try {
190                 connector.sendCommand(command);
191             } catch (KaleidescapeException e) {
192                 logger.warn("K Command: {} failed", command);
193             }
194         }
195     }
196
197     @Override
198     public void handleCommand(ChannelUID channelUID, Command command) {
199         String channel = channelUID.getId();
200
201         if (getThing().getStatus() != ThingStatus.ONLINE) {
202             logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
203             return;
204         }
205         synchronized (sequenceLock) {
206             if (!connector.isConnected()) {
207                 logger.debug("Command {} from channel {} is ignored: connection not established", command, channel);
208                 return;
209             }
210
211             try {
212                 if (command instanceof RefreshType) {
213                     handleRefresh(channel);
214                     return;
215                 }
216
217                 switch (channel) {
218                     case POWER:
219                         if (command instanceof OnOffType) {
220                             connector.sendCommand(command == OnOffType.ON ? LEAVE_STANDBY : ENTER_STANDBY);
221                         }
222                         break;
223                     case VOLUME:
224                         if (command instanceof PercentType) {
225                             this.volume = (int) ((PercentType) command).doubleValue();
226                             logger.debug("Got volume command {}", this.volume);
227                             connector.sendCommand(SEND_EVENT_VOLUME_LEVEL_EQ + this.volume);
228                         }
229                         break;
230                     case MUTE:
231                         if (command instanceof OnOffType) {
232                             this.isMuted = command == OnOffType.ON ? true : false;
233                         }
234                         connector.sendCommand(SEND_EVENT_MUTE + (this.isMuted ? MUTE_ON : MUTE_OFF));
235                         break;
236                     case MUSIC_REPEAT:
237                         if (command instanceof OnOffType) {
238                             connector.sendCommand(command == OnOffType.ON ? MUSIC_REPEAT_ON : MUSIC_REPEAT_OFF);
239                         }
240                         break;
241                     case MUSIC_RANDOM:
242                         if (command instanceof OnOffType) {
243                             connector.sendCommand(command == OnOffType.ON ? MUSIC_RANDOM_ON : MUSIC_RANDOM_OFF);
244                         }
245                         break;
246                     case CONTROL:
247                     case MUSIC_CONTROL:
248                         handleControlCommand(command);
249                         break;
250                     default:
251                         logger.debug("Command {} from channel {} failed: unexpected command", command, channel);
252                         break;
253                 }
254             } catch (KaleidescapeException e) {
255                 logger.debug("Command {} from channel {} failed: {}", command, channel, e.getMessage());
256                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
257                 closeConnection();
258                 scheduleReconnectJob();
259             }
260         }
261     }
262
263     /**
264      * Open the connection with the Kaleidescape component
265      *
266      * @return true if the connection is opened successfully or false if not
267      */
268     private synchronized boolean openConnection() {
269         connector.addEventListener(this);
270         try {
271             connector.open();
272         } catch (KaleidescapeException e) {
273             logger.debug("openConnection() failed: {}", e.getMessage());
274         }
275         logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
276         return connector.isConnected();
277     }
278
279     /**
280      * Close the connection with the Kaleidescape component
281      */
282     private synchronized void closeConnection() {
283         if (connector.isConnected()) {
284             connector.close();
285             connector.removeEventListener(this);
286             logger.debug("closeConnection(): disconnected");
287         }
288     }
289
290     @Override
291     public void onNewMessageEvent(KaleidescapeMessageEvent evt) {
292         lastEventReceived = System.currentTimeMillis();
293
294         // check if we are in standby
295         if (STANDBY_MSG.equals(evt.getKey())) {
296             if (!ThingStatusDetail.BRIDGE_OFFLINE.equals(thing.getStatusInfo().getStatusDetail())) {
297                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.BRIDGE_OFFLINE, STANDBY_MSG);
298             }
299             return;
300         }
301         try {
302             // Use the Enum valueOf to handle the message based on the event key. Otherwise there would be a huge
303             // case statement here
304             KaleidescapeMessageHandler.valueOf(evt.getKey()).handleMessage(evt.getValue(), this);
305
306             if (!evt.isCached()) {
307                 cache.put(evt.getKey(), evt.getValue());
308             }
309
310             if (ThingStatusDetail.BRIDGE_OFFLINE.equals(thing.getStatusInfo().getStatusDetail())) {
311                 // no longer in standby, update the status
312                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.friendlyName);
313             }
314         } catch (IllegalArgumentException e) {
315             logger.debug("Unhandled message: key {} = {}", evt.getKey(), evt.getValue());
316         }
317     }
318
319     /**
320      * Schedule the reconnection job
321      */
322     private void scheduleReconnectJob() {
323         logger.debug("Schedule reconnect job");
324         cancelReconnectJob();
325         reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
326             synchronized (sequenceLock) {
327                 if (!connector.isConnected()) {
328                     logger.debug("Trying to reconnect...");
329                     closeConnection();
330                     String error = EMPTY;
331                     if (openConnection()) {
332                         try {
333                             cache.clear();
334                             Set<String> initialCommands = new HashSet<>(Arrays.asList(GET_DEVICE_TYPE_NAME,
335                                     GET_FRIENDLY_NAME, GET_DEVICE_INFO, GET_SYSTEM_VERSION, GET_DEVICE_POWER_STATE,
336                                     GET_CINEMASCAPE_MASK, GET_CINEMASCAPE_MODE, GET_SCALE_MODE, GET_SCREEN_MASK,
337                                     GET_SCREEN_MASK2, GET_VIDEO_MODE, GET_UI_STATE, GET_HIGHLIGHTED_SELECTION,
338                                     GET_CHILD_MODE_STATE, GET_PLAY_STATUS, GET_MOVIE_LOCATION, GET_MOVIE_MEDIA_TYPE,
339                                     GET_PLAYING_TITLE_NAME));
340
341                             // Premiere Players and Cinema One support music
342                             if (thingTypeUID.equals(THING_TYPE_PLAYER) || thingTypeUID.equals(THING_TYPE_CINEMA_ONE)) {
343                                 initialCommands.addAll(Arrays.asList(GET_MUSIC_NOW_PLAYING_STATUS,
344                                         GET_MUSIC_PLAY_STATUS, GET_MUSIC_TITLE));
345                             }
346
347                             // everything after Premiere Player supports GET_SYSTEM_READINESS_STATE
348                             if (!thingTypeUID.equals(THING_TYPE_PLAYER)) {
349                                 initialCommands.add(GET_SYSTEM_READINESS_STATE);
350                             }
351
352                             // only Strato supports the GET_*_COLOR commands
353                             if (thingTypeUID.equals(THING_TYPE_STRATO)) {
354                                 initialCommands.addAll(Arrays.asList(GET_VIDEO_COLOR, GET_CONTENT_COLOR));
355                             }
356
357                             initialCommands.forEach(command -> {
358                                 try {
359                                     connector.sendCommand(command);
360                                 } catch (KaleidescapeException e) {
361                                     logger.debug("{}: {}", "Error sending initial commands", e.getMessage());
362                                 }
363                             });
364
365                             if (this.updatePeriod == 1) {
366                                 connector.sendCommand(SET_STATUS_CUE_PERIOD_1);
367                             }
368                         } catch (KaleidescapeException e) {
369                             error = "First command after connection failed";
370                             logger.debug("{}: {}", error, e.getMessage());
371                             closeConnection();
372                         }
373                     } else {
374                         error = "Reconnection failed";
375                     }
376                     if (!error.equals(EMPTY)) {
377                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
378                         return;
379                     }
380                     updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.friendlyName);
381                     lastEventReceived = System.currentTimeMillis();
382                 }
383             }
384         }, 1, RECON_POLLING_INTERVAL_S, TimeUnit.SECONDS);
385     }
386
387     /**
388      * Cancel the reconnection job
389      */
390     private void cancelReconnectJob() {
391         ScheduledFuture<?> reconnectJob = this.reconnectJob;
392         if (reconnectJob != null) {
393             reconnectJob.cancel(true);
394             this.reconnectJob = null;
395         }
396     }
397
398     /**
399      * Schedule the polling job
400      */
401     private void schedulePollingJob() {
402         logger.debug("Schedule polling job");
403         cancelPollingJob();
404
405         pollingJob = scheduler.scheduleWithFixedDelay(() -> {
406             synchronized (sequenceLock) {
407                 if (connector.isConnected()) {
408                     logger.debug("Polling the component for updated status...");
409                     try {
410                         connector.ping();
411                         cache.clear();
412                     } catch (KaleidescapeException e) {
413                         logger.debug("Polling error: {}", e.getMessage());
414                     }
415
416                     // if the last successful polling update was more than 1.25 intervals ago,
417                     // the component is not responding even though the connection is still good
418                     if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_S * 1.25 * 1000)) {
419                         logger.warn("Component not responding to status requests");
420                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
421                                 "Component not responding to status requests");
422                         closeConnection();
423                         scheduleReconnectJob();
424                     }
425                 }
426             }
427         }, POLLING_INTERVAL_S, POLLING_INTERVAL_S, TimeUnit.SECONDS);
428     }
429
430     /**
431      * Cancel the polling job
432      */
433     private void cancelPollingJob() {
434         ScheduledFuture<?> pollingJob = this.pollingJob;
435         if (pollingJob != null) {
436             pollingJob.cancel(true);
437             this.pollingJob = null;
438         }
439     }
440
441     private void handleControlCommand(Command command) throws KaleidescapeException {
442         if (command instanceof PlayPauseType) {
443             if (command == PlayPauseType.PLAY) {
444                 connector.sendCommand(PLAY);
445             } else if (command == PlayPauseType.PAUSE) {
446                 connector.sendCommand(PAUSE);
447             }
448         } else if (command instanceof NextPreviousType) {
449             if (command == NextPreviousType.NEXT) {
450                 connector.sendCommand(NEXT);
451             } else if (command == NextPreviousType.PREVIOUS) {
452                 connector.sendCommand(PREVIOUS);
453             }
454         } else if (command instanceof RewindFastforwardType) {
455             if (command == RewindFastforwardType.FASTFORWARD) {
456                 connector.sendCommand(SCAN_FORWARD);
457             } else if (command == RewindFastforwardType.REWIND) {
458                 connector.sendCommand(SCAN_REVERSE);
459             }
460         } else {
461             logger.warn("Unknown control command: {}", command);
462         }
463     }
464
465     private void handleRefresh(String channel) throws KaleidescapeException {
466         switch (channel) {
467             case POWER:
468                 connector.sendCommand(GET_DEVICE_POWER_STATE, cache.get("DEVICE_POWER_STATE"));
469                 break;
470             case VOLUME:
471                 updateState(channel, new PercentType(this.volume));
472                 break;
473             case MUTE:
474                 updateState(channel, this.isMuted ? OnOffType.ON : OnOffType.OFF);
475                 break;
476             case TITLE_NAME:
477                 connector.sendCommand(GET_PLAYING_TITLE_NAME, cache.get("TITLE_NAME"));
478                 break;
479             case PLAY_MODE:
480             case PLAY_SPEED:
481             case TITLE_NUM:
482             case TITLE_LENGTH:
483             case TITLE_LOC:
484             case CHAPTER_NUM:
485             case CHAPTER_LENGTH:
486             case CHAPTER_LOC:
487                 connector.sendCommand(GET_PLAY_STATUS, cache.get("PLAY_STATUS"));
488                 break;
489             case MOVIE_MEDIA_TYPE:
490                 connector.sendCommand(GET_MOVIE_MEDIA_TYPE, cache.get("MOVIE_MEDIA_TYPE"));
491                 break;
492             case MOVIE_LOCATION:
493                 connector.sendCommand(GET_MOVIE_LOCATION, cache.get("MOVIE_LOCATION"));
494                 break;
495             case VIDEO_MODE:
496             case VIDEO_MODE_COMPOSITE:
497             case VIDEO_MODE_COMPONENT:
498             case VIDEO_MODE_HDMI:
499                 connector.sendCommand(GET_VIDEO_MODE, cache.get("VIDEO_MODE"));
500                 break;
501             case VIDEO_COLOR:
502             case VIDEO_COLOR_EOTF:
503                 connector.sendCommand(GET_VIDEO_COLOR, cache.get("VIDEO_COLOR"));
504                 break;
505             case CONTENT_COLOR:
506             case CONTENT_COLOR_EOTF:
507                 connector.sendCommand(GET_CONTENT_COLOR, cache.get("CONTENT_COLOR"));
508                 break;
509             case SCALE_MODE:
510                 connector.sendCommand(GET_SCALE_MODE, cache.get("SCALE_MODE"));
511                 break;
512             case ASPECT_RATIO:
513             case SCREEN_MASK:
514                 connector.sendCommand(GET_SCREEN_MASK, cache.get("SCREEN_MASK"));
515                 break;
516             case SCREEN_MASK2:
517                 connector.sendCommand(GET_SCREEN_MASK2, cache.get("SCREEN_MASK2"));
518                 break;
519             case CINEMASCAPE_MASK:
520                 connector.sendCommand(GET_CINEMASCAPE_MASK, cache.get("GET_CINEMASCAPE_MASK"));
521                 break;
522             case CINEMASCAPE_MODE:
523                 connector.sendCommand(GET_CINEMASCAPE_MODE, cache.get("CINEMASCAPE_MODE"));
524                 break;
525             case UI_STATE:
526                 connector.sendCommand(GET_UI_STATE, cache.get("UI_STATE"));
527                 break;
528             case CHILD_MODE_STATE:
529                 connector.sendCommand(GET_CHILD_MODE_STATE, cache.get("CHILD_MODE_STATE"));
530                 break;
531             case SYSTEM_READINESS_STATE:
532                 connector.sendCommand(GET_SYSTEM_READINESS_STATE, cache.get("SYSTEM_READINESS_STATE"));
533                 break;
534             case HIGHLIGHTED_SELECTION:
535                 connector.sendCommand(GET_HIGHLIGHTED_SELECTION, cache.get("HIGHLIGHTED_SELECTION"));
536                 break;
537             case USER_DEFINED_EVENT:
538             case USER_INPUT:
539             case USER_INPUT_PROMPT:
540                 updateState(channel, StringType.EMPTY);
541                 break;
542             case MUSIC_REPEAT:
543             case MUSIC_RANDOM:
544                 connector.sendCommand(GET_MUSIC_NOW_PLAYING_STATUS, cache.get("MUSIC_NOW_PLAYING_STATUS"));
545                 break;
546             case MUSIC_TRACK:
547             case MUSIC_ARTIST:
548             case MUSIC_ALBUM:
549             case MUSIC_TRACK_HANDLE:
550             case MUSIC_ALBUM_HANDLE:
551             case MUSIC_NOWPLAY_HANDLE:
552                 connector.sendCommand(GET_MUSIC_TITLE, cache.get("MUSIC_TITLE"));
553                 break;
554             case MUSIC_PLAY_MODE:
555             case MUSIC_PLAY_SPEED:
556             case MUSIC_TRACK_LENGTH:
557             case MUSIC_TRACK_POSITION:
558             case MUSIC_TRACK_PROGRESS:
559                 connector.sendCommand(GET_MUSIC_PLAY_STATUS, cache.get("MUSIC_PLAY_STATUS"));
560                 break;
561             case DETAIL_TYPE:
562             case DETAIL_TITLE:
563             case DETAIL_ALBUM_TITLE:
564             case DETAIL_COVER_ART:
565             case DETAIL_COVER_URL:
566             case DETAIL_HIRES_COVER_URL:
567             case DETAIL_RATING:
568             case DETAIL_YEAR:
569             case DETAIL_RUNNING_TIME:
570             case DETAIL_ACTORS:
571             case DETAIL_ARTIST:
572             case DETAIL_DIRECTORS:
573             case DETAIL_GENRES:
574             case DETAIL_RATING_REASON:
575             case DETAIL_SYNOPSIS:
576             case DETAIL_REVIEW:
577             case DETAIL_COLOR_DESCRIPTION:
578             case DETAIL_COUNTRY:
579             case DETAIL_ASPECT_RATIO:
580             case DETAIL_DISC_LOCATION:
581                 updateState(channel, StringType.EMPTY);
582                 break;
583         }
584     }
585 }