]> git.basschouten.com Git - openhab-addons.git/blob
923c93c21c8d0bbbcba63bc22009e5057ea9cc27
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.samsungtv.internal.service;
14
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
16
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.List;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.samsungtv.internal.Utils;
26 import org.openhab.binding.samsungtv.internal.handler.SamsungTvHandler;
27 import org.openhab.binding.samsungtv.internal.protocol.KeyCode;
28 import org.openhab.binding.samsungtv.internal.protocol.RemoteController;
29 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerException;
30 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerLegacy;
31 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebSocket;
32 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
33 import org.openhab.core.io.net.http.WebSocketFactory;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.IncreaseDecreaseType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.library.types.RawType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.types.UpDownType;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 /**
47  * The {@link RemoteControllerService} is responsible for handling remote
48  * controller commands.
49  *
50  * @author Pauli Anttila - Initial contribution
51  * @author Martin van Wingerden - Some changes for manually configured devices
52  * @author Arjan Mels - Implemented websocket interface for recent TVs
53  * @author Nick Waterton - added power state monitoring for Frame TV's, some refactoring, sendkeys()
54  */
55 @NonNullByDefault
56 public class RemoteControllerService implements SamsungTvService {
57
58     private final Logger logger = LoggerFactory.getLogger(RemoteControllerService.class);
59
60     public static final String SERVICE_NAME = "RemoteControlReceiver";
61
62     private final List<String> supportedCommandsUpnp = Arrays.asList(KEY_CODE, POWER, CHANNEL);
63     private final List<String> supportedCommandsNonUpnp = Arrays.asList(KEY_CODE, VOLUME, MUTE, POWER, CHANNEL,
64             BROWSER_URL, STOP_BROWSER, SOURCE_APP);
65     private final List<String> supportedCommandsArt = Arrays.asList(ART_MODE, ART_JSON, ART_LABEL, ART_IMAGE,
66             ART_BRIGHTNESS, ART_COLOR_TEMPERATURE);
67     private static final List<String> REFRESH_CHANNELS = Arrays.asList();
68     private static final List<String> refreshArt = Arrays.asList(ART_BRIGHTNESS);
69     private static final List<String> refreshApps = Arrays.asList(SOURCE_APP);
70     private static final List<String> art2022 = Arrays.asList(ART_MODE, SET_ART_MODE);
71
72     private String host;
73     private boolean upnp;
74     private String previousApp = "None";
75     private final int keyTiming = 300;
76
77     private long busyUntil = System.currentTimeMillis();
78
79     public boolean artMode = false;
80     public boolean justStarted = true;
81     /* retry connection count */
82     private int retryCount = 0;
83
84     public final SamsungTvHandler handler;
85
86     private final RemoteController remoteController;
87
88     public RemoteControllerService(String host, int port, boolean upnp, SamsungTvHandler handler)
89             throws RemoteControllerException {
90         logger.debug("{}: Creating a Samsung TV RemoteController service: is UPNP:{}", host, upnp);
91         this.upnp = upnp;
92         this.host = host;
93         this.handler = handler;
94         try {
95             if (upnp) {
96                 remoteController = new RemoteControllerLegacy(host, port, "openHAB", "openHAB");
97                 remoteController.openConnection();
98             } else {
99                 remoteController = new RemoteControllerWebSocket(host, port, "openHAB", "openHAB", this);
100             }
101         } catch (RemoteControllerException e) {
102             throw new RemoteControllerException("Cannot create RemoteControllerService", e);
103         }
104     }
105
106     @Override
107     public String getServiceName() {
108         return SERVICE_NAME;
109     }
110
111     @Override
112     public List<String> getSupportedChannelNames(boolean refresh) {
113         // no refresh channels for UPNP remotecontroller
114         List<String> supported = new ArrayList<>(refresh ? upnp ? Arrays.asList() : REFRESH_CHANNELS
115                 : upnp ? supportedCommandsUpnp : supportedCommandsNonUpnp);
116         if (getArtModeSupported()) {
117             supported.addAll(refresh ? refreshArt : supportedCommandsArt);
118         }
119         if (getArtMode2022()) {
120             supported.addAll(refresh ? Arrays.asList() : art2022);
121         }
122         if (remoteController.noApps() && getPowerState() && refresh) {
123             supported.addAll(refreshApps);
124         }
125         if (!refresh) {
126             logger.trace("{}: getSupportedChannelNames: {}", host, supported);
127         }
128         return supported;
129     }
130
131     @Override
132     public boolean checkConnection() {
133         return remoteController.isConnected();
134     }
135
136     @Override
137     public void start() {
138         try {
139             if (!checkConnection()) {
140                 remoteController.openConnection();
141             }
142         } catch (RemoteControllerException e) {
143             reportError("Cannot connect to remote control service", e);
144         }
145         previousApp = "";
146     }
147
148     @Override
149     public void stop() {
150         try {
151             remoteController.close();
152         } catch (RemoteControllerException ignore) {
153             // ignore error
154         }
155     }
156
157     /**
158      * Clears the UPnP cache, or reconnects a websocket if diconnected
159      * Here we reconnect the websocket
160      */
161     @Override
162     public void clearCache() {
163         start();
164     }
165
166     @Override
167     public boolean isUpnp() {
168         return upnp;
169     }
170
171     @Override
172     public boolean handleCommand(String channel, Command command) {
173         logger.trace("{}: Received channel: {}, command: {}", host, channel, Utils.truncCmd(command));
174
175         boolean result = false;
176         if (!checkConnection() && !SET_ART_MODE.equals(channel)) {
177             logger.debug("{}: RemoteController is not connected", host);
178             if (getArtMode2022() && retryCount < 4) {
179                 retryCount += 1;
180                 logger.debug("{}: Reconnecting RemoteController, retry: {}", host, retryCount);
181                 start();
182                 return handler.handleCommand(channel, command, 3000);
183             } else {
184                 logger.warn("{}: TV is not responding - not reconnecting", host);
185             }
186             return false;
187         }
188         retryCount = 0;
189
190         if (command == RefreshType.REFRESH) {
191             switch (channel) {
192                 case SOURCE_APP:
193                     remoteController.updateCurrentApp();
194                     break;
195                 case ART_IMAGE:
196                 case ART_LABEL:
197                     remoteController.getArtmodeStatus("get_current_artwork");
198                     break;
199                 case ART_BRIGHTNESS:
200                     remoteController.getArtmodeStatus("get_brightness");
201                     break;
202                 case ART_COLOR_TEMPERATURE:
203                     remoteController.getArtmodeStatus("get_color_temperature");
204                     break;
205             }
206             return true;
207         }
208
209         switch (channel) {
210             case BROWSER_URL:
211                 if (command instanceof StringType) {
212                     remoteController.sendUrl(command.toString());
213                     result = true;
214                 }
215                 break;
216
217             case STOP_BROWSER:
218                 if (command instanceof OnOffType) {
219                     if (command.equals(OnOffType.ON)) {
220                         return handleCommand(SOURCE_APP, new StringType(""));
221                     } else {
222                         sendKeys(KeyCode.KEY_EXIT, 2000);
223                     }
224                     result = true;
225                 }
226                 break;
227
228             case SOURCE_APP:
229                 if (command instanceof StringType) {
230                     remoteController.sendSourceApp(command.toString());
231                     result = true;
232                 }
233                 break;
234
235             case POWER:
236                 if (command instanceof OnOffType) {
237                     if (!isUpnp()) {
238                         // websocket uses KEY_POWER
239                         if (OnOffType.ON.equals(command) != getPowerState()) {
240                             // send key only to toggle state
241                             sendKeys(KeyCode.KEY_POWER);
242                             if (getArtMode2022()) {
243                                 if (!getPowerState() & !artMode) {
244                                     // second key press to get out of art mode, once tv online
245                                     List<Object> commands = new ArrayList<>();
246                                     commands.add(9000);
247                                     commands.add(KeyCode.KEY_POWER);
248                                     sendKeys(commands);
249                                     updateArtMode(OnOffType.OFF.equals(command), 9000);
250                                 } else {
251                                     updateArtMode(OnOffType.OFF.equals(command), 1000);
252                                 }
253                             }
254                         }
255                     } else {
256                         // legacy controller uses KEY_POWERON/OFF
257                         if (command.equals(OnOffType.ON)) {
258                             sendKeys(KeyCode.KEY_POWERON);
259                         } else {
260                             sendKeys(KeyCode.KEY_POWEROFF);
261                         }
262                     }
263                     result = true;
264                 }
265                 break;
266
267             case SET_ART_MODE:
268                 // Used to manually set art mode for >=2022 Frame TV's
269                 logger.trace("{}: Setting Artmode to: {} artmode is: {}", host, command, artMode);
270                 if (command instanceof OnOffType) {
271                     handler.valueReceived(SET_ART_MODE, OnOffType.from(OnOffType.ON.equals(command)));
272                     if (OnOffType.ON.equals(command) != artMode || justStarted) {
273                         justStarted = false;
274                         updateArtMode(OnOffType.ON.equals(command));
275                     }
276                     result = true;
277                 }
278                 break;
279
280             case ART_MODE:
281                 if (command instanceof OnOffType) {
282                     // websocket uses KEY_POWER
283                     // send key only to toggle state when power = off
284                     if (!getPowerState()) {
285                         if (OnOffType.ON.equals(command)) {
286                             if (!artMode) {
287                                 sendKeys(KeyCode.KEY_POWER);
288                             }
289                         } else if (artMode) {
290                             // really switch off (long press of power)
291                             sendKeys(KeyCode.KEY_POWER, 4000);
292                         }
293                     } else {
294                         // switch TV off
295                         sendKeys(KeyCode.KEY_POWER);
296                     }
297                     if (getArtMode2022()) {
298                         if (OnOffType.ON.equals(command)) {
299                             if (!getPowerState()) {
300                                 // wait for TV to come online
301                                 updateArtMode(true, 3000);
302                             } else {
303                                 updateArtMode(true, 1000);
304                             }
305                         } else {
306                             this.artMode = false;
307                         }
308                     }
309                     result = true;
310                 }
311                 break;
312
313             case ART_JSON:
314                 if (command instanceof StringType) {
315                     String artJson = command.toString();
316                     if (!artJson.contains("\"id\"")) {
317                         artJson = artJson.replaceFirst("}$", ",}");
318                     }
319                     remoteController.getArtmodeStatus(artJson);
320                     result = true;
321                 }
322                 break;
323
324             case ART_IMAGE:
325             case ART_LABEL:
326                 if (command instanceof RawType) {
327                     remoteController.getArtmodeStatus("send_image", command.toFullString());
328                 } else if (command instanceof StringType) {
329                     if (command.toString().startsWith("data:image")) {
330                         remoteController.getArtmodeStatus("send_image", command.toString());
331                     } else if (channel.equals(ART_LABEL)) {
332                         remoteController.getArtmodeStatus("select_image", command.toString());
333                     }
334                     result = true;
335                 }
336                 break;
337
338             case ART_BRIGHTNESS:
339                 if (command instanceof DecimalType decimalCommand) {
340                     int value = decimalCommand.intValue();
341                     remoteController.getArtmodeStatus("set_brightness", String.valueOf(value / 10));
342                     result = true;
343                 }
344                 break;
345
346             case ART_COLOR_TEMPERATURE:
347                 if (command instanceof DecimalType decimalCommand) {
348                     int value = Math.max(-5, Math.min(decimalCommand.intValue(), 5));
349                     remoteController.getArtmodeStatus("set_color_temperature", String.valueOf(value));
350                     result = true;
351                 }
352                 break;
353
354             case KEY_CODE:
355                 if (command instanceof StringType) {
356                     // split on [, +], but not if encloded in "" or {}
357                     String[] cmds = command.toString().strip().split("(?=(?:(?:[^\"]*\"){2})*[^\"]*$)(?![^{]*})[, +]+",
358                             0);
359                     List<Object> commands = new ArrayList<>();
360                     for (String cmd : cmds) {
361                         try {
362                             logger.trace("{}: Procesing command: {}", host, cmd);
363                             if (cmd.startsWith("\"") || cmd.startsWith("{")) {
364                                 // remove leading and trailing "
365                                 cmd = cmd.replaceAll("^\"|\"$", "");
366                                 commands.add(cmd);
367                                 if (!cmd.startsWith("{")) {
368                                     commands.add("");
369                                 }
370                             } else if (cmd.matches("-?\\d{2,5}")) {
371                                 commands.add(Integer.parseInt(cmd));
372                             } else {
373                                 String ucmd = cmd.toUpperCase();
374                                 commands.add(KeyCode.valueOf(ucmd.startsWith("KEY_") ? ucmd : "KEY_" + ucmd));
375                             }
376                         } catch (IllegalArgumentException e) {
377                             logger.warn("{}: Remote control: unsupported cmd {} channel {}, {}", host, cmd, channel,
378                                     e.getMessage());
379                             return false;
380                         }
381                     }
382                     if (!commands.isEmpty()) {
383                         sendKeys(commands);
384                     }
385                     result = true;
386                 }
387                 break;
388
389             case MUTE:
390                 if (command instanceof OnOffType) {
391                     sendKeys(KeyCode.KEY_MUTE);
392                     result = true;
393                 }
394                 break;
395
396             case VOLUME:
397                 if (command instanceof UpDownType || command instanceof IncreaseDecreaseType) {
398                     if (command.equals(UpDownType.UP) || command.equals(IncreaseDecreaseType.INCREASE)) {
399                         sendKeys(KeyCode.KEY_VOLUP);
400                     } else {
401                         sendKeys(KeyCode.KEY_VOLDOWN);
402                     }
403                     result = true;
404                 }
405                 break;
406
407             case CHANNEL:
408                 if (command instanceof DecimalType decimalCommand) {
409                     KeyCode[] codes = String.valueOf(decimalCommand.intValue()).chars()
410                             .mapToObj(c -> KeyCode.valueOf("KEY_" + String.valueOf((char) c))).toArray(KeyCode[]::new);
411                     List<Object> commands = new ArrayList<>(Arrays.asList(codes));
412                     commands.add(KeyCode.KEY_ENTER);
413                     sendKeys(commands);
414                     result = true;
415                 }
416                 break;
417             default:
418                 logger.warn("{}: Remote control: unsupported channel: {}", host, channel);
419                 return false;
420         }
421         if (!result) {
422             logger.warn("{}: Remote control: wrong command type {} channel {}", host, command, channel);
423         }
424         return result;
425     }
426
427     public synchronized void sendKeys(KeyCode key, int press) {
428         sendKeys(Arrays.asList(key), press);
429     }
430
431     public synchronized void sendKeys(KeyCode key) {
432         sendKeys(Arrays.asList(key), 0);
433     }
434
435     public synchronized void sendKeys(List<Object> keys) {
436         sendKeys(keys, 0);
437     }
438
439     /**
440      * Send sequence of key codes to Samsung TV RemoteController instance.
441      * 300 ms between each key click. If press is > 0 then send key press/release
442      *
443      * @param keys List containing key codes/Integer delays to send.
444      *            if integer delays are negative, send key press of abs(delay)
445      * @param press int value of length of keypress in ms (0 means Click)
446      */
447     public synchronized void sendKeys(List<Object> keys, int press) {
448         int timingInMs = keyTiming;
449         int delay = (int) Math.max(0, busyUntil - System.currentTimeMillis());
450         @Nullable
451         ScheduledExecutorService scheduler = getScheduler();
452         if (scheduler == null) {
453             logger.warn("{}: Unable to schedule key sequence", host);
454             return;
455         }
456         for (int i = 0; i < keys.size(); i++) {
457             Object key = keys.get(i);
458             if (key instanceof Integer keyAsInt) {
459                 if (keyAsInt > 0) {
460                     delay += Math.max(0, keyAsInt - (2 * timingInMs));
461                 } else {
462                     press = Math.max(timingInMs, Math.abs(keyAsInt));
463                     delay -= timingInMs;
464                 }
465                 continue;
466             }
467             if (press == 0 && key instanceof KeyCode && key.equals(KeyCode.KEY_BT_VOICE)) {
468                 press = 3000;
469                 delay -= timingInMs;
470             }
471             int duration = press;
472             scheduler.schedule(() -> {
473                 if (duration > 0) {
474                     remoteController.sendKeyPress((KeyCode) key, duration);
475                 } else {
476                     if (key instanceof String keyAsString) {
477                         remoteController.sendKey(keyAsString);
478                     } else {
479                         remoteController.sendKey((KeyCode) key);
480                     }
481                 }
482             }, (i * timingInMs) + delay, TimeUnit.MILLISECONDS);
483             delay += press;
484             press = 0;
485         }
486         busyUntil = System.currentTimeMillis() + (keys.size() * timingInMs) + delay;
487         logger.trace("{}: Key Sequence Queued", host);
488     }
489
490     private void reportError(String message, RemoteControllerException e) {
491         reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
492     }
493
494     private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
495         handler.reportError(statusDetail, message, e);
496     }
497
498     public void appsUpdated(List<String> apps) {
499         // do nothing
500     }
501
502     public void updateCurrentApp() {
503         remoteController.updateCurrentApp();
504     }
505
506     public synchronized void currentAppUpdated(String app) {
507         if (!previousApp.equals(app)) {
508             handler.valueReceived(SOURCE_APP, new StringType(app));
509             previousApp = app;
510         }
511     }
512
513     public void updateArtMode(boolean artMode, int ms) {
514         @Nullable
515         ScheduledExecutorService scheduler = getScheduler();
516         if (scheduler == null) {
517             logger.warn("{}: Unable to schedule art mode update", host);
518         } else {
519             scheduler.schedule(() -> {
520                 updateArtMode(artMode);
521             }, ms, TimeUnit.MILLISECONDS);
522         }
523     }
524
525     public synchronized void updateArtMode(boolean artMode) {
526         // manual update of power/art mode for >=2022 frame TV's
527         if (this.artMode == artMode) {
528             logger.debug("{}: Artmode setting is already: {}", host, artMode);
529             return;
530         }
531         if (artMode) {
532             logger.debug("{}: Setting power state OFF, Art Mode ON", host);
533             powerUpdated(false, true);
534         } else {
535             logger.debug("{}: Setting power state ON, Art Mode OFF", host);
536             powerUpdated(true, false);
537         }
538         if (this.artMode) {
539             currentAppUpdated("artMode");
540         } else {
541             currentAppUpdated("");
542         }
543         handler.valueReceived(SET_ART_MODE, OnOffType.from(this.artMode));
544         if (!remoteController.noApps()) {
545             updateCurrentApp();
546         }
547     }
548
549     public void powerUpdated(boolean on, boolean artMode) {
550         String powerState = fetchPowerState();
551         if (!getArtMode2022()) {
552             setArtModeSupported(true);
553         }
554         if (!"on".equals(powerState)) {
555             on = false;
556             artMode = false;
557             currentAppUpdated("");
558         }
559         setPowerState(on);
560         this.artMode = artMode;
561         // order of state updates is important to prevent extraneous transitions in overall state
562         if (on) {
563             handler.valueReceived(POWER, OnOffType.from(on));
564             handler.valueReceived(ART_MODE, OnOffType.from(artMode));
565         } else {
566             handler.valueReceived(ART_MODE, OnOffType.from(artMode));
567             handler.valueReceived(POWER, OnOffType.from(on));
568         }
569     }
570
571     public boolean getArtMode2022() {
572         return handler.getArtMode2022();
573     }
574
575     public void setArtMode2022(boolean artmode) {
576         handler.setArtMode2022(artmode);
577     }
578
579     public boolean getArtModeSupported() {
580         return handler.getArtModeSupported();
581     }
582
583     public void setArtModeSupported(boolean artmode) {
584         handler.setArtModeSupported(artmode);
585     }
586
587     public boolean getPowerState() {
588         return handler.getPowerState();
589     }
590
591     public void setPowerState(boolean power) {
592         handler.setPowerState(power);
593     }
594
595     public String fetchPowerState() {
596         return handler.fetchPowerState();
597     }
598
599     public void setOffline() {
600         handler.setOffline();
601     }
602
603     public void putConfig(String key, String value) {
604         handler.putConfig(key, value);
605     }
606
607     public @Nullable ScheduledExecutorService getScheduler() {
608         return handler.getScheduler();
609     }
610
611     public @Nullable WebSocketFactory getWebSocketFactory() {
612         return handler.getWebSocketFactory();
613     }
614 }