2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.samsungtv.internal.service;
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
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;
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;
47 * The {@link RemoteControllerService} is responsible for handling remote
48 * controller commands.
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()
56 public class RemoteControllerService implements SamsungTvService {
58 private final Logger logger = LoggerFactory.getLogger(RemoteControllerService.class);
60 public static final String SERVICE_NAME = "RemoteControlReceiver";
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);
74 private String previousApp = "None";
75 private final int keyTiming = 300;
77 private long busyUntil = System.currentTimeMillis();
79 public boolean artMode = false;
80 public boolean justStarted = true;
81 /* retry connection count */
82 private int retryCount = 0;
84 public final SamsungTvHandler handler;
86 private final RemoteController remoteController;
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);
93 this.handler = handler;
96 remoteController = new RemoteControllerLegacy(host, port, "openHAB", "openHAB");
97 remoteController.openConnection();
99 remoteController = new RemoteControllerWebSocket(host, port, "openHAB", "openHAB", this);
101 } catch (RemoteControllerException e) {
102 throw new RemoteControllerException("Cannot create RemoteControllerService", e);
107 public String getServiceName() {
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);
119 if (getArtMode2022()) {
120 supported.addAll(refresh ? Arrays.asList() : art2022);
122 if (remoteController.noApps() && getPowerState() && refresh) {
123 supported.addAll(refreshApps);
126 logger.trace("{}: getSupportedChannelNames: {}", host, supported);
132 public boolean checkConnection() {
133 return remoteController.isConnected();
137 public void start() {
139 if (!checkConnection()) {
140 remoteController.openConnection();
142 } catch (RemoteControllerException e) {
143 reportError("Cannot connect to remote control service", e);
151 remoteController.close();
152 } catch (RemoteControllerException ignore) {
158 * Clears the UPnP cache, or reconnects a websocket if diconnected
159 * Here we reconnect the websocket
162 public void clearCache() {
167 public boolean isUpnp() {
172 public boolean handleCommand(String channel, Command command) {
173 logger.trace("{}: Received channel: {}, command: {}", host, channel, Utils.truncCmd(command));
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) {
180 logger.debug("{}: Reconnecting RemoteController, retry: {}", host, retryCount);
182 return handler.handleCommand(channel, command, 3000);
184 logger.warn("{}: TV is not responding - not reconnecting", host);
190 if (command == RefreshType.REFRESH) {
193 remoteController.updateCurrentApp();
197 remoteController.getArtmodeStatus("get_current_artwork");
200 remoteController.getArtmodeStatus("get_brightness");
202 case ART_COLOR_TEMPERATURE:
203 remoteController.getArtmodeStatus("get_color_temperature");
205 case ART_ORIENTATION:
206 remoteController.getArtmodeStatus("get_current_rotation");
214 if (command instanceof StringType) {
215 remoteController.sendUrl(command.toString());
221 if (command instanceof OnOffType) {
222 if (command.equals(OnOffType.ON)) {
223 return handleCommand(SOURCE_APP, new StringType(""));
225 sendKeys(KeyCode.KEY_EXIT, 2000);
232 if (command instanceof StringType) {
233 remoteController.sendSourceApp(command.toString());
239 if (command instanceof OnOffType) {
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<>();
250 commands.add(KeyCode.KEY_POWER);
252 updateArtMode(OnOffType.OFF.equals(command), 9000);
254 updateArtMode(OnOffType.OFF.equals(command), 1000);
259 // legacy controller uses KEY_POWERON/OFF
260 if (command.equals(OnOffType.ON)) {
261 sendKeys(KeyCode.KEY_POWERON);
263 sendKeys(KeyCode.KEY_POWEROFF);
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) {
277 updateArtMode(OnOffType.ON.equals(command));
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)) {
290 sendKeys(KeyCode.KEY_POWER);
292 } else if (artMode) {
293 // really switch off (long press of power)
294 sendKeys(KeyCode.KEY_POWER, 4000);
298 sendKeys(KeyCode.KEY_POWER);
300 if (getArtMode2022()) {
301 if (OnOffType.ON.equals(command)) {
302 if (!getPowerState()) {
303 // wait for TV to come online
304 updateArtMode(true, 3000);
306 updateArtMode(true, 1000);
309 this.artMode = false;
317 if (command instanceof StringType) {
318 String artJson = command.toString();
319 if (!artJson.contains("\"id\"")) {
320 artJson = artJson.replaceFirst("}$", ",}");
322 remoteController.getArtmodeStatus(artJson);
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());
342 if (command instanceof DecimalType decimalCommand) {
343 int value = decimalCommand.intValue();
344 remoteController.getArtmodeStatus("set_brightness", String.valueOf(value / 10));
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));
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);
368 if (command instanceof StringType) {
369 // split on [, +], but not if encloded in "" or {}
370 String[] cmds = command.toString().strip().split("(?=(?:(?:[^\"]*\"){2})*[^\"]*$)(?![^{]*})[, +]+",
372 List<Object> commands = new ArrayList<>();
373 for (String cmd : cmds) {
375 logger.trace("{}: Procesing command: {}", host, cmd);
376 if (cmd.startsWith("\"") || cmd.startsWith("{")) {
377 // remove leading and trailing "
378 cmd = cmd.replaceAll("^\"|\"$", "");
380 if (!cmd.startsWith("{")) {
383 } else if (cmd.matches("-?\\d{2,5}")) {
384 commands.add(Integer.parseInt(cmd));
386 String ucmd = cmd.toUpperCase();
387 commands.add(KeyCode.valueOf(ucmd.startsWith("KEY_") ? ucmd : "KEY_" + ucmd));
389 } catch (IllegalArgumentException e) {
390 logger.warn("{}: Remote control: unsupported cmd {} channel {}, {}", host, cmd, channel,
395 if (!commands.isEmpty()) {
403 if (command instanceof OnOffType) {
404 sendKeys(KeyCode.KEY_MUTE);
410 if (command instanceof UpDownType || command instanceof IncreaseDecreaseType) {
411 if (command.equals(UpDownType.UP) || command.equals(IncreaseDecreaseType.INCREASE)) {
412 sendKeys(KeyCode.KEY_VOLUP);
414 sendKeys(KeyCode.KEY_VOLDOWN);
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);
431 logger.warn("{}: Remote control: unsupported channel: {}", host, channel);
435 logger.warn("{}: Remote control: wrong command type {} channel {}", host, command, channel);
440 public synchronized void sendKeys(KeyCode key, int press) {
441 sendKeys(Arrays.asList(key), press);
444 public synchronized void sendKeys(KeyCode key) {
445 sendKeys(Arrays.asList(key), 0);
448 public synchronized void sendKeys(List<Object> keys) {
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
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)
460 public synchronized void sendKeys(List<Object> keys, int press) {
461 int timingInMs = keyTiming;
462 int delay = (int) Math.max(0, busyUntil - System.currentTimeMillis());
464 ScheduledExecutorService scheduler = getScheduler();
465 if (scheduler == null) {
466 logger.warn("{}: Unable to schedule key sequence", host);
469 for (int i = 0; i < keys.size(); i++) {
470 Object key = keys.get(i);
471 if (key instanceof Integer keyAsInt) {
473 delay += Math.max(0, keyAsInt - (2 * timingInMs));
475 press = Math.max(timingInMs, Math.abs(keyAsInt));
480 if (press == 0 && key instanceof KeyCode && key.equals(KeyCode.KEY_BT_VOICE)) {
484 int duration = press;
485 scheduler.schedule(() -> {
487 remoteController.sendKeyPress((KeyCode) key, duration);
489 if (key instanceof String keyAsString) {
490 remoteController.sendKey(keyAsString);
492 remoteController.sendKey((KeyCode) key);
495 }, (i * timingInMs) + delay, TimeUnit.MILLISECONDS);
499 busyUntil = System.currentTimeMillis() + (keys.size() * timingInMs) + delay;
500 logger.trace("{}: Key Sequence Queued", host);
503 private void reportError(String message, RemoteControllerException e) {
504 reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
507 private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
508 handler.reportError(statusDetail, message, e);
511 public void appsUpdated(List<String> apps) {
515 public void updateCurrentApp() {
516 remoteController.updateCurrentApp();
519 public synchronized void currentAppUpdated(String app) {
520 if (!previousApp.equals(app)) {
521 handler.valueReceived(SOURCE_APP, new StringType(app));
526 public void updateArtMode(boolean artMode, int ms) {
528 ScheduledExecutorService scheduler = getScheduler();
529 if (scheduler == null) {
530 logger.warn("{}: Unable to schedule art mode update", host);
532 scheduler.schedule(() -> {
533 updateArtMode(artMode);
534 }, ms, TimeUnit.MILLISECONDS);
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);
545 logger.debug("{}: Setting power state OFF, Art Mode ON", host);
546 powerUpdated(false, true);
548 logger.debug("{}: Setting power state ON, Art Mode OFF", host);
549 powerUpdated(true, false);
552 currentAppUpdated("artMode");
554 currentAppUpdated("");
556 handler.valueReceived(SET_ART_MODE, OnOffType.from(this.artMode));
557 if (!remoteController.noApps()) {
562 public void powerUpdated(boolean on, boolean artMode) {
563 String powerState = fetchPowerState();
564 if (!getArtMode2022()) {
565 setArtModeSupported(true);
567 if (!"on".equals(powerState)) {
570 currentAppUpdated("");
573 this.artMode = artMode;
574 // order of state updates is important to prevent extraneous transitions in overall state
576 handler.valueReceived(POWER, OnOffType.from(on));
577 handler.valueReceived(ART_MODE, OnOffType.from(artMode));
579 handler.valueReceived(ART_MODE, OnOffType.from(artMode));
580 handler.valueReceived(POWER, OnOffType.from(on));
584 public boolean getArtMode2022() {
585 return handler.getArtMode2022();
588 public void setArtMode2022(boolean artmode) {
589 handler.setArtMode2022(artmode);
592 public boolean getArtModeSupported() {
593 return handler.getArtModeSupported();
596 public void setArtModeSupported(boolean artmode) {
597 handler.setArtModeSupported(artmode);
600 public boolean getPowerState() {
601 return handler.getPowerState();
604 public void setPowerState(boolean power) {
605 handler.setPowerState(power);
608 public String fetchPowerState() {
609 return handler.fetchPowerState();
612 public void setOffline() {
613 handler.setOffline();
616 public void putConfig(String key, String value) {
617 handler.putConfig(key, value);
620 public @Nullable ScheduledExecutorService getScheduler() {
621 return handler.getScheduler();
624 public @Nullable WebSocketFactory getWebSocketFactory() {
625 return handler.getWebSocketFactory();