]> git.basschouten.com Git - openhab-addons.git/blob
e3e291efd525af69fe9e0812cbc1c4f87cee37f1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.io.IOException;
18 import java.io.InputStreamReader;
19 import java.net.URI;
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;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.concurrent.CopyOnWriteArraySet;
28
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;
50
51 import com.google.gson.Gson;
52
53 /**
54  * The {@link RemoteControllerService} is responsible for handling remote
55  * controller commands.
56  *
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
60  */
61 @NonNullByDefault
62 public class RemoteControllerService implements SamsungTvService, RemoteControllerWebsocketCallback {
63
64     private final Logger logger = LoggerFactory.getLogger(RemoteControllerService.class);
65
66     public static final String SERVICE_NAME = "RemoteControlReceiver";
67
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);
71
72     private String host;
73     private int port;
74     private boolean upnp;
75
76     boolean power = true;
77     boolean artMode = false;
78
79     private boolean artModeSupported = false;
80
81     private Set<EventListener> listeners = new CopyOnWriteArraySet<>();
82
83     private @Nullable RemoteController remoteController = null;
84
85     /** Path for the information endpoint (note the final slash!) */
86     private static final String WS_ENDPOINT_V2 = "/api/v2/";
87
88     /** Description of the json returned for the information endpoint */
89     @NonNullByDefault({})
90     static class TVProperties {
91         @NonNullByDefault({})
92         static class Device {
93             boolean FrameTVSupport;
94             boolean GamePadSupport;
95             boolean ImeSyncedSupport;
96             String OS;
97             boolean TokenAuthSupport;
98             boolean VoiceSupport;
99             String countryCode;
100             String description;
101             String firmwareVersion;
102             String modelName;
103             String name;
104             String networkType;
105             String resolution;
106         }
107
108         Device device;
109         String isSupport;
110     }
111
112     /**
113      * Discover the type of remote control service the TV supports.
114      *
115      * @param hostname
116      * @return map with properties containing at least the protocol and port
117      */
118     public static Map<String, Object> discover(String hostname) {
119         Map<String, Object> result = new HashMap<>();
120
121         try {
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);
128             return result;
129         } catch (RemoteControllerException e) {
130             // ignore error
131         }
132
133         URI uri;
134         try {
135             uri = new URI("http", null, hostname, SamsungTvConfiguration.PORT_DEFAULT_WEBSOCKET, WS_ENDPOINT_V2, null,
136                     null);
137             InputStreamReader reader = new InputStreamReader(uri.toURL().openStream());
138             TVProperties properties = new Gson().fromJson(reader, TVProperties.class);
139
140             if (properties.device.TokenAuthSupport) {
141                 result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_SECUREWEBSOCKET);
142                 result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_SECUREWEBSOCKET);
143             } else {
144                 result.put(SamsungTvConfiguration.PROTOCOL, SamsungTvConfiguration.PROTOCOL_WEBSOCKET);
145                 result.put(SamsungTvConfiguration.PORT, SamsungTvConfiguration.PORT_DEFAULT_WEBSOCKET);
146             }
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);
150         }
151
152         return result;
153     }
154
155     private RemoteControllerService(String host, int port, boolean upnp) {
156         logger.debug("Creating a Samsung TV RemoteController service: {}", upnp);
157         this.upnp = upnp;
158         this.host = host;
159         this.port = port;
160     }
161
162     static RemoteControllerService createUpnpService(String host, int port) {
163         return new RemoteControllerService(host, port, true);
164     }
165
166     public static RemoteControllerService createNonUpnpService(String host, int port) {
167         return new RemoteControllerService(host, port, false);
168     }
169
170     @Override
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);
176         }
177         logger.trace("getSupportedChannelNames: {}", supported);
178         return supported;
179     }
180
181     @Override
182     public void addEventListener(EventListener listener) {
183         listeners.add(listener);
184     }
185
186     @Override
187     public void removeEventListener(EventListener listener) {
188         listeners.remove(listener);
189     }
190
191     public boolean checkConnection() {
192         if (remoteController != null) {
193             return remoteController.isConnected();
194         } else {
195             return false;
196         }
197     }
198
199     @Override
200     public void start() {
201         if (remoteController != null) {
202             try {
203                 remoteController.openConnection();
204             } catch (RemoteControllerException e) {
205                 logger.warn("Cannot open remote interface ({})", e.getMessage());
206             }
207             return;
208         }
209
210         String protocol = (String) getConfig(SamsungTvConfiguration.PROTOCOL);
211         logger.info("Using {} interface", protocol);
212
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)) {
217             try {
218                 remoteController = new RemoteControllerWebSocket(host, port, "openHAB", "openHAB", this);
219             } catch (RemoteControllerException e) {
220                 reportError("Cannot connect to remote control service", e);
221             }
222         } else {
223             remoteController = null;
224             return;
225         }
226
227         if (remoteController != null) {
228             try {
229                 remoteController.openConnection();
230             } catch (RemoteControllerException e) {
231                 reportError("Cannot connect to remote control service", e);
232             }
233         }
234     }
235
236     @Override
237     public void stop() {
238         if (remoteController != null) {
239             try {
240                 remoteController.close();
241             } catch (RemoteControllerException ignore) {
242             }
243         }
244     }
245
246     @Override
247     public void clearCache() {
248     }
249
250     @Override
251     public boolean isUpnp() {
252         return upnp;
253     }
254
255     @Override
256     public void handleCommand(String channel, Command command) {
257         logger.trace("Received channel: {}, command: {}", channel, command);
258         if (command == RefreshType.REFRESH) {
259             return;
260         }
261
262         if (remoteController == null) {
263             return;
264         }
265
266         KeyCode key = null;
267
268         if (remoteController instanceof RemoteControllerWebSocket) {
269             RemoteControllerWebSocket remoteControllerWebSocket = (RemoteControllerWebSocket) remoteController;
270             switch (channel) {
271                 case BROWSER_URL:
272                     if (command instanceof StringType) {
273                         remoteControllerWebSocket.sendUrl(command.toString());
274                     } else {
275                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
276                     }
277                     return;
278                 case SOURCE_APP:
279                     if (command instanceof StringType) {
280                         remoteControllerWebSocket.sendSourceApp(command.toString());
281                     } else {
282                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
283                     }
284                     return;
285                 case POWER:
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);
291                         }
292                     } else {
293                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
294                     }
295                     return;
296                 case ART_MODE:
297                     if (command instanceof OnOffType) {
298                         // websocket uses KEY_POWER
299                         // send key only to toggle state when power = off
300                         if (!power) {
301                             if (OnOffType.ON.equals(command)) {
302                                 if (!artMode) {
303                                     sendKeyCode(KeyCode.KEY_POWER);
304                                 }
305                             } else {
306                                 sendKeyCodePress(KeyCode.KEY_POWER);
307                                 // really switch off
308                             }
309                         } else {
310                             // switch TV off
311                             sendKeyCode(KeyCode.KEY_POWER);
312                             // switch TV to art mode
313                             sendKeyCode(KeyCode.KEY_POWER);
314                         }
315                     } else {
316                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
317                     }
318                     return;
319             }
320         }
321
322         switch (channel) {
323             case KEY_CODE:
324                 if (command instanceof StringType) {
325                     try {
326                         key = KeyCode.valueOf(command.toString().toUpperCase());
327                     } catch (IllegalArgumentException e) {
328                         try {
329                             key = KeyCode.valueOf("KEY_" + command.toString().toUpperCase());
330                         } catch (IllegalArgumentException e2) {
331                             // do nothing, error message is logged later
332                         }
333                     }
334
335                     if (key != null) {
336                         sendKeyCode(key);
337                     } else {
338                         logger.warn("Remote control: Command '{}' not supported for channel '{}'", command, channel);
339                     }
340                 } else {
341                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
342                 }
343                 return;
344
345             case POWER:
346                 if (command instanceof OnOffType) {
347                     // legacy controller uses KEY_POWERON/OFF
348                     if (command.equals(OnOffType.ON)) {
349                         sendKeyCode(KeyCode.KEY_POWERON);
350                     } else {
351                         sendKeyCode(KeyCode.KEY_POWEROFF);
352                     }
353                 } else {
354                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
355                 }
356                 return;
357
358             case MUTE:
359                 sendKeyCode(KeyCode.KEY_MUTE);
360                 return;
361
362             case VOLUME:
363                 if (command instanceof UpDownType) {
364                     if (command.equals(UpDownType.UP)) {
365                         sendKeyCode(KeyCode.KEY_VOLUP);
366                     } else {
367                         sendKeyCode(KeyCode.KEY_VOLDOWN);
368                     }
369                 } else {
370                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
371                 }
372                 return;
373
374             case 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;
380                     int num1 = val % 10;
381
382                     List<KeyCode> commands = new ArrayList<>();
383
384                     if (num4 > 0) {
385                         commands.add(KeyCode.valueOf("KEY_" + num4));
386                     }
387                     if (num4 > 0 || num3 > 0) {
388                         commands.add(KeyCode.valueOf("KEY_" + num3));
389                     }
390                     if (num4 > 0 || num3 > 0 || num2 > 0) {
391                         commands.add(KeyCode.valueOf("KEY_" + num2));
392                     }
393                     commands.add(KeyCode.valueOf("KEY_" + num1));
394                     commands.add(KeyCode.KEY_ENTER);
395                     sendKeyCodes(commands);
396                 } else {
397                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
398                 }
399                 return;
400             default:
401                 logger.warn("Remote control: unsupported channel: {}", channel);
402         }
403     }
404
405     /**
406      * Sends a command to Samsung TV device.
407      *
408      * @param key Button code to send
409      */
410     private void sendKeyCode(KeyCode key) {
411         try {
412             if (remoteController != null) {
413                 remoteController.sendKey(key);
414             }
415         } catch (RemoteControllerException e) {
416             reportError(String.format("Could not send command to device on %s:%d", host, port), e);
417         }
418     }
419
420     private void sendKeyCodePress(KeyCode key) {
421         try {
422             if (remoteController != null && remoteController instanceof RemoteControllerWebSocket) {
423                 ((RemoteControllerWebSocket) remoteController).sendKeyPress(key);
424             }
425         } catch (RemoteControllerException e) {
426             reportError(String.format("Could not send command to device on %s:%d", host, port), e);
427         }
428     }
429
430     /**
431      * Sends a sequence of command to Samsung TV device.
432      *
433      * @param keys List of button codes to send
434      */
435     private void sendKeyCodes(final List<KeyCode> keys) {
436         try {
437             if (remoteController != null) {
438                 remoteController.sendKeys(keys);
439             }
440         } catch (RemoteControllerException e) {
441             reportError(String.format("Could not send command to device on %s:%d", host, port), e);
442         }
443     }
444
445     private void reportError(String message, RemoteControllerException e) {
446         reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
447     }
448
449     private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
450         for (EventListener listener : listeners) {
451             listener.reportError(statusDetail, message, e);
452         }
453     }
454
455     @Override
456     public void appsUpdated(List<String> apps) {
457         // do nothing
458     }
459
460     @Override
461     public void currentAppUpdated(@Nullable String app) {
462         for (EventListener listener : listeners) {
463             listener.valueReceived(SOURCE_APP, new StringType(app));
464         }
465     }
466
467     @Override
468     public void powerUpdated(boolean on, boolean artmode) {
469         artModeSupported = true;
470         power = on;
471         this.artMode = artmode;
472
473         for (EventListener listener : listeners) {
474             // order of state updates is important to prevent extraneous transitions in overall state
475             if (on) {
476                 listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
477                 listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
478             } else {
479                 listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
480                 listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
481             }
482         }
483     }
484
485     @Override
486     public void connectionError(@Nullable Throwable error) {
487         logger.debug("Connection error: {}", error != null ? error.getMessage() : "");
488         remoteController = null;
489     }
490
491     public boolean isArtModeSupported() {
492         return artModeSupported;
493     }
494
495     @Override
496     public void putConfig(String key, Object value) {
497         for (EventListener listener : listeners) {
498             listener.putConfig(key, value);
499         }
500     }
501
502     @Override
503     public @Nullable Object getConfig(String key) {
504         for (EventListener listener : listeners) {
505             return listener.getConfig(key);
506         }
507         return null;
508     }
509
510     @Override
511     public @Nullable WebSocketFactory getWebSocketFactory() {
512         for (EventListener listener : listeners) {
513             return listener.getWebSocketFactory();
514         }
515         return null;
516     }
517 }