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.io.IOException;
18 import java.io.InputStreamReader;
20 import java.net.URISyntaxException;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.HashMap;
24 import java.util.List;
27 import java.util.concurrent.CopyOnWriteArraySet;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
32 import org.openhab.binding.samsungtv.internal.protocol.KeyCode;
33 import org.openhab.binding.samsungtv.internal.protocol.RemoteController;
34 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerException;
35 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerLegacy;
36 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebSocket;
37 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerWebsocketCallback;
38 import org.openhab.binding.samsungtv.internal.service.api.EventListener;
39 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
40 import org.openhab.core.io.net.http.WebSocketFactory;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.library.types.UpDownType;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
51 import com.google.gson.Gson;
54 * The {@link RemoteControllerService} is responsible for handling remote
55 * controller commands.
57 * @author Pauli Anttila - Initial contribution
58 * @author Martin van Wingerden - Some changes for manually configured devices
59 * @author Arjan Mels - Implemented websocket interface for recent TVs
62 public class RemoteControllerService implements SamsungTvService, RemoteControllerWebsocketCallback {
64 private final Logger logger = LoggerFactory.getLogger(RemoteControllerService.class);
66 public static final String SERVICE_NAME = "RemoteControlReceiver";
68 private final List<String> supportedCommandsUpnp = Arrays.asList(KEY_CODE, POWER, CHANNEL);
69 private final List<String> supportedCommandsNonUpnp = Arrays.asList(KEY_CODE, VOLUME, MUTE, POWER, CHANNEL);
70 private final List<String> extraSupportedCommandsWebSocket = Arrays.asList(BROWSER_URL, SOURCE_APP, ART_MODE);
77 boolean artMode = false;
79 private boolean artModeSupported = false;
81 private Set<EventListener> listeners = new CopyOnWriteArraySet<>();
83 private @Nullable RemoteController remoteController = null;
85 /** Path for the information endpoint (note the final slash!) */
86 private static final String WS_ENDPOINT_V2 = "/api/v2/";
88 /** Description of the json returned for the information endpoint */
90 static class TVProperties {
93 boolean FrameTVSupport;
94 boolean GamePadSupport;
95 boolean ImeSyncedSupport;
97 boolean TokenAuthSupport;
101 String firmwareVersion;
113 * Discover the type of remote control service the TV supports.
116 * @return map with properties containing at least the protocol and port
118 public static Map<String, Object> discover(String hostname) {
119 Map<String, Object> result = new HashMap<>();
122 RemoteControllerLegacy remoteController = new RemoteControllerLegacy(hostname,
123 SamsungTvConfiguration.PORT_DEFAULT_LEGACY, "openHAB", "openHAB");
124 remoteController.openConnection();
125 remoteController.close();
126 result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_LEGACY);
127 result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_LEGACY);
129 } catch (RemoteControllerException e) {
135 uri = new URI("http", null, hostname, SamsungTvConfiguration.PORT_DEFAULT_WEBSOCKET, WS_ENDPOINT_V2, null,
137 InputStreamReader reader = new InputStreamReader(uri.toURL().openStream());
138 TVProperties properties = new Gson().fromJson(reader, TVProperties.class);
140 if (properties.device.TokenAuthSupport) {
141 result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET);
142 result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_SECUREWEBSOCKET);
144 result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_WEBSOCKET);
145 result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_WEBSOCKET);
147 } catch (URISyntaxException | IOException e) {
148 LoggerFactory.getLogger(RemoteControllerService.class).debug("Cannot retrieve info from TV", e);
149 result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_NONE);
155 private RemoteControllerService(String host, int port, boolean upnp) {
156 logger.debug("Creating a Samsung TV RemoteController service: {}", upnp);
162 static RemoteControllerService createUpnpService(String host, int port) {
163 return new RemoteControllerService(host, port, true);
166 public static RemoteControllerService createNonUpnpService(String host, int port) {
167 return new RemoteControllerService(host, port, false);
171 public List<String> getSupportedChannelNames() {
172 List<String> supported = upnp ? supportedCommandsUpnp : supportedCommandsNonUpnp;
173 if (remoteController instanceof RemoteControllerWebSocket) {
174 supported = new ArrayList<>(supported);
175 supported.addAll(extraSupportedCommandsWebSocket);
177 logger.trace("getSupportedChannelNames: {}", supported);
182 public void addEventListener(EventListener listener) {
183 listeners.add(listener);
187 public void removeEventListener(EventListener listener) {
188 listeners.remove(listener);
191 public boolean checkConnection() {
192 if (remoteController != null) {
193 return remoteController.isConnected();
200 public void start() {
201 if (remoteController != null) {
203 remoteController.openConnection();
204 } catch (RemoteControllerException e) {
205 logger.warn("Cannot open remote interface ({})", e.getMessage());
210 String protocol = (String) getConfig(SamsungTvConfiguration.PROTOCOL);
211 logger.info("Using {} interface", protocol);
213 if (SamsungTvConfiguration.PROTOCOL_LEGACY.equals(protocol)) {
214 remoteController = new RemoteControllerLegacy(host, port, "openHAB", "openHAB");
215 } else if (SamsungTvConfiguration.PROTOCOL_WEBSOCKET.equals(protocol)
216 || SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET.equals(protocol)) {
218 remoteController = new RemoteControllerWebSocket(host, port, "openHAB", "openHAB", this);
219 } catch (RemoteControllerException e) {
220 reportError("Cannot connect to remote control service", e);
223 remoteController = null;
227 if (remoteController != null) {
229 remoteController.openConnection();
230 } catch (RemoteControllerException e) {
231 reportError("Cannot connect to remote control service", e);
238 if (remoteController != null) {
240 remoteController.close();
241 } catch (RemoteControllerException ignore) {
247 public void clearCache() {
251 public boolean isUpnp() {
256 public void handleCommand(String channel, Command command) {
257 logger.trace("Received channel: {}, command: {}", channel, command);
258 if (command == RefreshType.REFRESH) {
262 if (remoteController == null) {
268 if (remoteController instanceof RemoteControllerWebSocket remoteControllerWebSocket) {
271 if (command instanceof StringType) {
272 remoteControllerWebSocket.sendUrl(command.toString());
274 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
278 if (command instanceof StringType) {
279 remoteControllerWebSocket.sendSourceApp(command.toString());
281 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
285 if (command instanceof OnOffType) {
286 // websocket uses KEY_POWER
287 // send key only to toggle state
288 if (OnOffType.ON.equals(command) != power) {
289 sendKeyCode(KeyCode.KEY_POWER);
292 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
296 if (command instanceof OnOffType) {
297 // websocket uses KEY_POWER
298 // send key only to toggle state when power = off
300 if (OnOffType.ON.equals(command)) {
302 sendKeyCode(KeyCode.KEY_POWER);
305 sendKeyCodePress(KeyCode.KEY_POWER);
310 sendKeyCode(KeyCode.KEY_POWER);
311 // switch TV to art mode
312 sendKeyCode(KeyCode.KEY_POWER);
315 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
323 if (command instanceof StringType) {
325 key = KeyCode.valueOf(command.toString().toUpperCase());
326 } catch (IllegalArgumentException e) {
328 key = KeyCode.valueOf("KEY_" + command.toString().toUpperCase());
329 } catch (IllegalArgumentException e2) {
330 // do nothing, error message is logged later
337 logger.warn("Remote control: Command '{}' not supported for channel '{}'", command, channel);
340 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
345 if (command instanceof OnOffType) {
346 // legacy controller uses KEY_POWERON/OFF
347 if (command.equals(OnOffType.ON)) {
348 sendKeyCode(KeyCode.KEY_POWERON);
350 sendKeyCode(KeyCode.KEY_POWEROFF);
353 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
358 sendKeyCode(KeyCode.KEY_MUTE);
362 if (command instanceof UpDownType) {
363 if (command.equals(UpDownType.UP)) {
364 sendKeyCode(KeyCode.KEY_VOLUP);
366 sendKeyCode(KeyCode.KEY_VOLDOWN);
369 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
374 if (command instanceof DecimalType decimalCommand) {
375 int val = decimalCommand.intValue();
376 int num4 = val / 1000 % 10;
377 int num3 = val / 100 % 10;
378 int num2 = val / 10 % 10;
381 List<KeyCode> commands = new ArrayList<>();
384 commands.add(KeyCode.valueOf("KEY_" + num4));
386 if (num4 > 0 || num3 > 0) {
387 commands.add(KeyCode.valueOf("KEY_" + num3));
389 if (num4 > 0 || num3 > 0 || num2 > 0) {
390 commands.add(KeyCode.valueOf("KEY_" + num2));
392 commands.add(KeyCode.valueOf("KEY_" + num1));
393 commands.add(KeyCode.KEY_ENTER);
394 sendKeyCodes(commands);
396 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
400 logger.warn("Remote control: unsupported channel: {}", channel);
405 * Sends a command to Samsung TV device.
407 * @param key Button code to send
409 private void sendKeyCode(KeyCode key) {
411 if (remoteController != null) {
412 remoteController.sendKey(key);
414 } catch (RemoteControllerException e) {
415 reportError(String.format("Could not send command to device on %s:%d", host, port), e);
419 private void sendKeyCodePress(KeyCode key) {
421 if (remoteController instanceof RemoteControllerWebSocket remoteControllerWebSocket) {
422 remoteControllerWebSocket.sendKeyPress(key);
424 } catch (RemoteControllerException e) {
425 reportError(String.format("Could not send command to device on %s:%d", host, port), e);
430 * Sends a sequence of command to Samsung TV device.
432 * @param keys List of button codes to send
434 private void sendKeyCodes(final List<KeyCode> keys) {
436 if (remoteController != null) {
437 remoteController.sendKeys(keys);
439 } catch (RemoteControllerException e) {
440 reportError(String.format("Could not send command to device on %s:%d", host, port), e);
444 private void reportError(String message, RemoteControllerException e) {
445 reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
448 private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
449 for (EventListener listener : listeners) {
450 listener.reportError(statusDetail, message, e);
455 public void appsUpdated(List<String> apps) {
460 public void currentAppUpdated(@Nullable String app) {
461 for (EventListener listener : listeners) {
462 listener.valueReceived(SOURCE_APP, new StringType(app));
467 public void powerUpdated(boolean on, boolean artmode) {
468 artModeSupported = true;
470 this.artMode = artmode;
472 for (EventListener listener : listeners) {
473 // order of state updates is important to prevent extraneous transitions in overall state
475 listener.valueReceived(POWER, OnOffType.from(on));
476 listener.valueReceived(ART_MODE, OnOffType.from(artmode));
478 listener.valueReceived(ART_MODE, OnOffType.from(artmode));
479 listener.valueReceived(POWER, OnOffType.from(on));
485 public void connectionError(@Nullable Throwable error) {
486 logger.debug("Connection error: {}", error != null ? error.getMessage() : "");
487 remoteController = null;
490 public boolean isArtModeSupported() {
491 return artModeSupported;
495 public void putConfig(String key, Object value) {
496 for (EventListener listener : listeners) {
497 listener.putConfig(key, value);
502 public @Nullable Object getConfig(String key) {
503 for (EventListener listener : listeners) {
504 return listener.getConfig(key);
510 public @Nullable WebSocketFactory getWebSocketFactory() {
511 for (EventListener listener : listeners) {
512 return listener.getWebSocketFactory();