]> git.basschouten.com Git - openhab-addons.git/blob
6e6e694971829a7868ae3b04ab1019409116ea8f
[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, ART_ORIENTATION);
67     private static final List<String> REFRESH_CHANNELS = Arrays.asList();
68     private static final List<String> refreshArt = Arrays.asList(ART_BRIGHTNESS, ART_ORIENTATION);
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                 case ART_ORIENTATION:
206                     remoteController.getArtmodeStatus("get_current_rotation");
207                     break;
208             }
209             return true;
210         }
211
212         switch (channel) {
213             case BROWSER_URL:
214                 if (command instanceof StringType) {
215                     remoteController.sendUrl(command.toString());
216                     result = true;
217                 }
218                 break;
219
220             case STOP_BROWSER:
221                 if (command instanceof OnOffType) {
222                     if (command.equals(OnOffType.ON)) {
223                         return handleCommand(SOURCE_APP, new StringType(""));
224                     } else {
225                         sendKeys(KeyCode.KEY_EXIT, 2000);
226                     }
227                     result = true;
228                 }
229                 break;
230
231             case SOURCE_APP:
232                 if (command instanceof StringType) {
233                     remoteController.sendSourceApp(command.toString());
234                     result = true;
235                 }
236                 break;
237
238             case POWER:
239                 if (command instanceof OnOffType) {
240                     if (!isUpnp()) {
241                         // websocket uses KEY_POWER
242                         if (OnOffType.ON.equals(command) != getPowerState()) {
243                             // send key only to toggle state
244                             sendKeys(KeyCode.KEY_POWER);
245                             if (getArtMode2022()) {
246                                 if (!getPowerState() & !artMode) {
247                                     // second key press to get out of art mode, once tv online
248                                     List<Object> commands = new ArrayList<>();
249                                     commands.add(9000);
250                                     commands.add(KeyCode.KEY_POWER);
251                                     sendKeys(commands);
252                                     updateArtMode(OnOffType.OFF.equals(command), 9000);
253                                 } else {
254                                     updateArtMode(OnOffType.OFF.equals(command), 1000);
255                                 }
256                             }
257                         }
258                     } else {
259                         // legacy controller uses KEY_POWERON/OFF
260                         if (command.equals(OnOffType.ON)) {
261                             sendKeys(KeyCode.KEY_POWERON);
262                         } else {
263                             sendKeys(KeyCode.KEY_POWEROFF);
264                         }
265                     }
266                     result = true;
267                 }
268                 break;
269
270             case SET_ART_MODE:
271                 // Used to manually set art mode for >=2022 Frame TV's
272                 logger.trace("{}: Setting Artmode to: {} artmode is: {}", host, command, artMode);
273                 if (command instanceof OnOffType) {
274                     handler.valueReceived(SET_ART_MODE, OnOffType.from(OnOffType.ON.equals(command)));
275                     if (OnOffType.ON.equals(command) != artMode || justStarted) {
276                         justStarted = false;
277                         updateArtMode(OnOffType.ON.equals(command));
278                     }
279                     result = true;
280                 }
281                 break;
282
283             case ART_MODE:
284                 if (command instanceof OnOffType) {
285                     // websocket uses KEY_POWER
286                     // send key only to toggle state when power = off
287                     if (!getPowerState()) {
288                         if (OnOffType.ON.equals(command)) {
289                             if (!artMode) {
290                                 sendKeys(KeyCode.KEY_POWER);
291                             }
292                         } else if (artMode) {
293                             // really switch off (long press of power)
294                             sendKeys(KeyCode.KEY_POWER, 4000);
295                         }
296                     } else {
297                         // switch TV off
298                         sendKeys(KeyCode.KEY_POWER);
299                     }
300                     if (getArtMode2022()) {
301                         if (OnOffType.ON.equals(command)) {
302                             if (!getPowerState()) {
303                                 // wait for TV to come online
304                                 updateArtMode(true, 3000);
305                             } else {
306                                 updateArtMode(true, 1000);
307                             }
308                         } else {
309                             this.artMode = false;
310                         }
311                     }
312                     result = true;
313                 }
314                 break;
315
316             case ART_JSON:
317                 if (command instanceof StringType) {
318                     String artJson = command.toString();
319                     if (!artJson.contains("\"id\"")) {
320                         artJson = artJson.replaceFirst("}$", ",}");
321                     }
322                     remoteController.getArtmodeStatus(artJson);
323                     result = true;
324                 }
325                 break;
326
327             case ART_IMAGE:
328             case ART_LABEL:
329                 if (command instanceof RawType) {
330                     remoteController.getArtmodeStatus("send_image", command.toFullString());
331                 } else if (command instanceof StringType) {
332                     if (command.toString().startsWith("data:image")) {
333                         remoteController.getArtmodeStatus("send_image", command.toString());
334                     } else if (channel.equals(ART_LABEL)) {
335                         remoteController.getArtmodeStatus("select_image", command.toString());
336                     }
337                     result = true;
338                 }
339                 break;
340
341             case ART_BRIGHTNESS:
342                 if (command instanceof DecimalType decimalCommand) {
343                     int value = decimalCommand.intValue();
344                     remoteController.getArtmodeStatus("set_brightness", String.valueOf(value / 10));
345                     result = true;
346                 }
347                 break;
348
349             case ART_COLOR_TEMPERATURE:
350                 if (command instanceof DecimalType decimalCommand) {
351                     int value = Math.max(-5, Math.min(decimalCommand.intValue(), 5));
352                     remoteController.getArtmodeStatus("set_color_temperature", String.valueOf(value));
353                     result = true;
354                 }
355                 break;
356
357             case ART_ORIENTATION:
358                 if (command instanceof OnOffType) {
359                     String key = handler.configuration.getOrientationKey();
360                     if (!key.isBlank()) {
361                         sendKeys(KeyCode.valueOf(key), 4000);
362                         result = true;
363                     }
364                 }
365                 break;
366
367             case KEY_CODE:
368                 if (command instanceof StringType) {
369                     // split on [, +], but not if encloded in "" or {}
370                     String[] cmds = command.toString().strip().split("(?=(?:(?:[^\"]*\"){2})*[^\"]*$)(?![^{]*})[, +]+",
371                             0);
372                     List<Object> commands = new ArrayList<>();
373                     for (String cmd : cmds) {
374                         try {
375                             logger.trace("{}: Procesing command: {}", host, cmd);
376                             if (cmd.startsWith("\"") || cmd.startsWith("{")) {
377                                 // remove leading and trailing "
378                                 cmd = cmd.replaceAll("^\"|\"$", "");
379                                 commands.add(cmd);
380                                 if (!cmd.startsWith("{")) {
381                                     commands.add("");
382                                 }
383                             } else if (cmd.matches("-?\\d{2,5}")) {
384                                 commands.add(Integer.parseInt(cmd));
385                             } else {
386                                 String ucmd = cmd.toUpperCase();
387                                 commands.add(KeyCode.valueOf(ucmd.startsWith("KEY_") ? ucmd : "KEY_" + ucmd));
388                             }
389                         } catch (IllegalArgumentException e) {
390                             logger.warn("{}: Remote control: unsupported cmd {} channel {}, {}", host, cmd, channel,
391                                     e.getMessage());
392                             return false;
393                         }
394                     }
395                     if (!commands.isEmpty()) {
396                         sendKeys(commands);
397                     }
398                     result = true;
399                 }
400                 break;
401
402             case MUTE:
403                 if (command instanceof OnOffType) {
404                     sendKeys(KeyCode.KEY_MUTE);
405                     result = true;
406                 }
407                 break;
408
409             case VOLUME:
410                 if (command instanceof UpDownType || command instanceof IncreaseDecreaseType) {
411                     if (command.equals(UpDownType.UP) || command.equals(IncreaseDecreaseType.INCREASE)) {
412                         sendKeys(KeyCode.KEY_VOLUP);
413                     } else {
414                         sendKeys(KeyCode.KEY_VOLDOWN);
415                     }
416                     result = true;
417                 }
418                 break;
419
420             case CHANNEL:
421                 if (command instanceof DecimalType decimalCommand) {
422                     KeyCode[] codes = String.valueOf(decimalCommand.intValue()).chars()
423                             .mapToObj(c -> KeyCode.valueOf("KEY_" + String.valueOf((char) c))).toArray(KeyCode[]::new);
424                     List<Object> commands = new ArrayList<>(Arrays.asList(codes));
425                     commands.add(KeyCode.KEY_ENTER);
426                     sendKeys(commands);
427                     result = true;
428                 }
429                 break;
430             default:
431                 logger.warn("{}: Remote control: unsupported channel: {}", host, channel);
432                 return false;
433         }
434         if (!result) {
435             logger.warn("{}: Remote control: wrong command type {} channel {}", host, command, channel);
436         }
437         return result;
438     }
439
440     public synchronized void sendKeys(KeyCode key, int press) {
441         sendKeys(Arrays.asList(key), press);
442     }
443
444     public synchronized void sendKeys(KeyCode key) {
445         sendKeys(Arrays.asList(key), 0);
446     }
447
448     public synchronized void sendKeys(List<Object> keys) {
449         sendKeys(keys, 0);
450     }
451
452     /**
453      * Send sequence of key codes to Samsung TV RemoteController instance.
454      * 300 ms between each key click. If press is > 0 then send key press/release
455      *
456      * @param keys List containing key codes/Integer delays to send.
457      *            if integer delays are negative, send key press of abs(delay)
458      * @param press int value of length of keypress in ms (0 means Click)
459      */
460     public synchronized void sendKeys(List<Object> keys, int press) {
461         int timingInMs = keyTiming;
462         int delay = (int) Math.max(0, busyUntil - System.currentTimeMillis());
463         @Nullable
464         ScheduledExecutorService scheduler = getScheduler();
465         if (scheduler == null) {
466             logger.warn("{}: Unable to schedule key sequence", host);
467             return;
468         }
469         for (int i = 0; i < keys.size(); i++) {
470             Object key = keys.get(i);
471             if (key instanceof Integer keyAsInt) {
472                 if (keyAsInt > 0) {
473                     delay += Math.max(0, keyAsInt - (2 * timingInMs));
474                 } else {
475                     press = Math.max(timingInMs, Math.abs(keyAsInt));
476                     delay -= timingInMs;
477                 }
478                 continue;
479             }
480             if (press == 0 && key instanceof KeyCode && key.equals(KeyCode.KEY_BT_VOICE)) {
481                 press = 3000;
482                 delay -= timingInMs;
483             }
484             int duration = press;
485             scheduler.schedule(() -> {
486                 if (duration > 0) {
487                     remoteController.sendKeyPress((KeyCode) key, duration);
488                 } else {
489                     if (key instanceof String keyAsString) {
490                         remoteController.sendKey(keyAsString);
491                     } else {
492                         remoteController.sendKey((KeyCode) key);
493                     }
494                 }
495             }, (i * timingInMs) + delay, TimeUnit.MILLISECONDS);
496             delay += press;
497             press = 0;
498         }
499         busyUntil = System.currentTimeMillis() + (keys.size() * timingInMs) + delay;
500         logger.trace("{}: Key Sequence Queued", host);
501     }
502
503     private void reportError(String message, RemoteControllerException e) {
504         reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
505     }
506
507     private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
508         handler.reportError(statusDetail, message, e);
509     }
510
511     public void appsUpdated(List<String> apps) {
512         // do nothing
513     }
514
515     public void updateCurrentApp() {
516         remoteController.updateCurrentApp();
517     }
518
519     public synchronized void currentAppUpdated(String app) {
520         if (!previousApp.equals(app)) {
521             handler.valueReceived(SOURCE_APP, new StringType(app));
522             previousApp = app;
523         }
524     }
525
526     public void updateArtMode(boolean artMode, int ms) {
527         @Nullable
528         ScheduledExecutorService scheduler = getScheduler();
529         if (scheduler == null) {
530             logger.warn("{}: Unable to schedule art mode update", host);
531         } else {
532             scheduler.schedule(() -> {
533                 updateArtMode(artMode);
534             }, ms, TimeUnit.MILLISECONDS);
535         }
536     }
537
538     public synchronized void updateArtMode(boolean artMode) {
539         // manual update of power/art mode for >=2022 frame TV's
540         if (this.artMode == artMode) {
541             logger.debug("{}: Artmode setting is already: {}", host, artMode);
542             return;
543         }
544         if (artMode) {
545             logger.debug("{}: Setting power state OFF, Art Mode ON", host);
546             powerUpdated(false, true);
547         } else {
548             logger.debug("{}: Setting power state ON, Art Mode OFF", host);
549             powerUpdated(true, false);
550         }
551         if (this.artMode) {
552             currentAppUpdated("artMode");
553         } else {
554             currentAppUpdated("");
555         }
556         handler.valueReceived(SET_ART_MODE, OnOffType.from(this.artMode));
557         if (!remoteController.noApps()) {
558             updateCurrentApp();
559         }
560     }
561
562     public void powerUpdated(boolean on, boolean artMode) {
563         String powerState = fetchPowerState();
564         if (!getArtMode2022()) {
565             setArtModeSupported(true);
566         }
567         if (!"on".equals(powerState)) {
568             on = false;
569             artMode = false;
570             currentAppUpdated("");
571         }
572         setPowerState(on);
573         this.artMode = artMode;
574         // order of state updates is important to prevent extraneous transitions in overall state
575         if (on) {
576             handler.valueReceived(POWER, OnOffType.from(on));
577             handler.valueReceived(ART_MODE, OnOffType.from(artMode));
578         } else {
579             handler.valueReceived(ART_MODE, OnOffType.from(artMode));
580             handler.valueReceived(POWER, OnOffType.from(on));
581         }
582     }
583
584     public boolean getArtMode2022() {
585         return handler.getArtMode2022();
586     }
587
588     public void setArtMode2022(boolean artmode) {
589         handler.setArtMode2022(artmode);
590     }
591
592     public boolean getArtModeSupported() {
593         return handler.getArtModeSupported();
594     }
595
596     public void setArtModeSupported(boolean artmode) {
597         handler.setArtModeSupported(artmode);
598     }
599
600     public boolean getPowerState() {
601         return handler.getPowerState();
602     }
603
604     public void setPowerState(boolean power) {
605         handler.setPowerState(power);
606     }
607
608     public String fetchPowerState() {
609         return handler.fetchPowerState();
610     }
611
612     public void setOffline() {
613         handler.setOffline();
614     }
615
616     public void putConfig(String key, String value) {
617         handler.putConfig(key, value);
618     }
619
620     public @Nullable ScheduledExecutorService getScheduler() {
621         return handler.getScheduler();
622     }
623
624     public @Nullable WebSocketFactory getWebSocketFactory() {
625         return handler.getWebSocketFactory();
626     }
627 }