2 * Copyright (c) 2010-2023 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) {
269 RemoteControllerWebSocket remoteControllerWebSocket = (RemoteControllerWebSocket) remoteController;
272 if (command instanceof StringType) {
273 remoteControllerWebSocket.sendUrl(command.toString());
275 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
279 if (command instanceof StringType) {
280 remoteControllerWebSocket.sendSourceApp(command.toString());
282 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
286 if (command instanceof OnOffType) {
287 // websocket uses KEY_POWER
288 // send key only to toggle state
289 if (OnOffType.ON.equals(command) != power) {
290 sendKeyCode(KeyCode.KEY_POWER);
293 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
297 if (command instanceof OnOffType) {
298 // websocket uses KEY_POWER
299 // send key only to toggle state when power = off
301 if (OnOffType.ON.equals(command)) {
303 sendKeyCode(KeyCode.KEY_POWER);
306 sendKeyCodePress(KeyCode.KEY_POWER);
311 sendKeyCode(KeyCode.KEY_POWER);
312 // switch TV to art mode
313 sendKeyCode(KeyCode.KEY_POWER);
316 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
324 if (command instanceof StringType) {
326 key = KeyCode.valueOf(command.toString().toUpperCase());
327 } catch (IllegalArgumentException e) {
329 key = KeyCode.valueOf("KEY_" + command.toString().toUpperCase());
330 } catch (IllegalArgumentException e2) {
331 // do nothing, error message is logged later
338 logger.warn("Remote control: Command '{}' not supported for channel '{}'", command, channel);
341 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
346 if (command instanceof OnOffType) {
347 // legacy controller uses KEY_POWERON/OFF
348 if (command.equals(OnOffType.ON)) {
349 sendKeyCode(KeyCode.KEY_POWERON);
351 sendKeyCode(KeyCode.KEY_POWEROFF);
354 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
359 sendKeyCode(KeyCode.KEY_MUTE);
363 if (command instanceof UpDownType) {
364 if (command.equals(UpDownType.UP)) {
365 sendKeyCode(KeyCode.KEY_VOLUP);
367 sendKeyCode(KeyCode.KEY_VOLDOWN);
370 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
375 if (command instanceof DecimalType) {
376 int val = ((DecimalType) command).intValue();
377 int num4 = val / 1000 % 10;
378 int num3 = val / 100 % 10;
379 int num2 = val / 10 % 10;
382 List<KeyCode> commands = new ArrayList<>();
385 commands.add(KeyCode.valueOf("KEY_" + num4));
387 if (num4 > 0 || num3 > 0) {
388 commands.add(KeyCode.valueOf("KEY_" + num3));
390 if (num4 > 0 || num3 > 0 || num2 > 0) {
391 commands.add(KeyCode.valueOf("KEY_" + num2));
393 commands.add(KeyCode.valueOf("KEY_" + num1));
394 commands.add(KeyCode.KEY_ENTER);
395 sendKeyCodes(commands);
397 logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
401 logger.warn("Remote control: unsupported channel: {}", channel);
406 * Sends a command to Samsung TV device.
408 * @param key Button code to send
410 private void sendKeyCode(KeyCode key) {
412 if (remoteController != null) {
413 remoteController.sendKey(key);
415 } catch (RemoteControllerException e) {
416 reportError(String.format("Could not send command to device on %s:%d", host, port), e);
420 private void sendKeyCodePress(KeyCode key) {
422 if (remoteController != null && remoteController instanceof RemoteControllerWebSocket) {
423 ((RemoteControllerWebSocket) remoteController).sendKeyPress(key);
425 } catch (RemoteControllerException e) {
426 reportError(String.format("Could not send command to device on %s:%d", host, port), e);
431 * Sends a sequence of command to Samsung TV device.
433 * @param keys List of button codes to send
435 private void sendKeyCodes(final List<KeyCode> keys) {
437 if (remoteController != null) {
438 remoteController.sendKeys(keys);
440 } catch (RemoteControllerException e) {
441 reportError(String.format("Could not send command to device on %s:%d", host, port), e);
445 private void reportError(String message, RemoteControllerException e) {
446 reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
449 private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
450 for (EventListener listener : listeners) {
451 listener.reportError(statusDetail, message, e);
456 public void appsUpdated(List<String> apps) {
461 public void currentAppUpdated(@Nullable String app) {
462 for (EventListener listener : listeners) {
463 listener.valueReceived(SOURCE_APP, new StringType(app));
468 public void powerUpdated(boolean on, boolean artmode) {
469 artModeSupported = true;
471 this.artMode = artmode;
473 for (EventListener listener : listeners) {
474 // order of state updates is important to prevent extraneous transitions in overall state
476 listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
477 listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
479 listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
480 listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
486 public void connectionError(@Nullable Throwable error) {
487 logger.debug("Connection error: {}", error != null ? error.getMessage() : "");
488 remoteController = null;
491 public boolean isArtModeSupported() {
492 return artModeSupported;
496 public void putConfig(String key, Object value) {
497 for (EventListener listener : listeners) {
498 listener.putConfig(key, value);
503 public @Nullable Object getConfig(String key) {
504 for (EventListener listener : listeners) {
505 return listener.getConfig(key);
511 public @Nullable WebSocketFactory getWebSocketFactory() {
512 for (EventListener listener : listeners) {
513 return listener.getWebSocketFactory();