]> git.basschouten.com Git - openhab-addons.git/blob
054324ec910cd0623e7082f92cd67282c38c3aa1
[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 remoteControllerWebSocket) {
269             switch (channel) {
270                 case BROWSER_URL:
271                     if (command instanceof StringType) {
272                         remoteControllerWebSocket.sendUrl(command.toString());
273                     } else {
274                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
275                     }
276                     return;
277                 case SOURCE_APP:
278                     if (command instanceof StringType) {
279                         remoteControllerWebSocket.sendSourceApp(command.toString());
280                     } else {
281                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
282                     }
283                     return;
284                 case POWER:
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);
290                         }
291                     } else {
292                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
293                     }
294                     return;
295                 case ART_MODE:
296                     if (command instanceof OnOffType) {
297                         // websocket uses KEY_POWER
298                         // send key only to toggle state when power = off
299                         if (!power) {
300                             if (OnOffType.ON.equals(command)) {
301                                 if (!artMode) {
302                                     sendKeyCode(KeyCode.KEY_POWER);
303                                 }
304                             } else {
305                                 sendKeyCodePress(KeyCode.KEY_POWER);
306                                 // really switch off
307                             }
308                         } else {
309                             // switch TV off
310                             sendKeyCode(KeyCode.KEY_POWER);
311                             // switch TV to art mode
312                             sendKeyCode(KeyCode.KEY_POWER);
313                         }
314                     } else {
315                         logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
316                     }
317                     return;
318             }
319         }
320
321         switch (channel) {
322             case KEY_CODE:
323                 if (command instanceof StringType) {
324                     try {
325                         key = KeyCode.valueOf(command.toString().toUpperCase());
326                     } catch (IllegalArgumentException e) {
327                         try {
328                             key = KeyCode.valueOf("KEY_" + command.toString().toUpperCase());
329                         } catch (IllegalArgumentException e2) {
330                             // do nothing, error message is logged later
331                         }
332                     }
333
334                     if (key != null) {
335                         sendKeyCode(key);
336                     } else {
337                         logger.warn("Remote control: Command '{}' not supported for channel '{}'", command, channel);
338                     }
339                 } else {
340                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
341                 }
342                 return;
343
344             case POWER:
345                 if (command instanceof OnOffType) {
346                     // legacy controller uses KEY_POWERON/OFF
347                     if (command.equals(OnOffType.ON)) {
348                         sendKeyCode(KeyCode.KEY_POWERON);
349                     } else {
350                         sendKeyCode(KeyCode.KEY_POWEROFF);
351                     }
352                 } else {
353                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
354                 }
355                 return;
356
357             case MUTE:
358                 sendKeyCode(KeyCode.KEY_MUTE);
359                 return;
360
361             case VOLUME:
362                 if (command instanceof UpDownType) {
363                     if (command.equals(UpDownType.UP)) {
364                         sendKeyCode(KeyCode.KEY_VOLUP);
365                     } else {
366                         sendKeyCode(KeyCode.KEY_VOLDOWN);
367                     }
368                 } else {
369                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
370                 }
371                 return;
372
373             case 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;
379                     int num1 = val % 10;
380
381                     List<KeyCode> commands = new ArrayList<>();
382
383                     if (num4 > 0) {
384                         commands.add(KeyCode.valueOf("KEY_" + num4));
385                     }
386                     if (num4 > 0 || num3 > 0) {
387                         commands.add(KeyCode.valueOf("KEY_" + num3));
388                     }
389                     if (num4 > 0 || num3 > 0 || num2 > 0) {
390                         commands.add(KeyCode.valueOf("KEY_" + num2));
391                     }
392                     commands.add(KeyCode.valueOf("KEY_" + num1));
393                     commands.add(KeyCode.KEY_ENTER);
394                     sendKeyCodes(commands);
395                 } else {
396                     logger.warn("Remote control: unsupported command type {} for channel {}", command, channel);
397                 }
398                 return;
399             default:
400                 logger.warn("Remote control: unsupported channel: {}", channel);
401         }
402     }
403
404     /**
405      * Sends a command to Samsung TV device.
406      *
407      * @param key Button code to send
408      */
409     private void sendKeyCode(KeyCode key) {
410         try {
411             if (remoteController != null) {
412                 remoteController.sendKey(key);
413             }
414         } catch (RemoteControllerException e) {
415             reportError(String.format("Could not send command to device on %s:%d", host, port), e);
416         }
417     }
418
419     private void sendKeyCodePress(KeyCode key) {
420         try {
421             if (remoteController instanceof RemoteControllerWebSocket remoteControllerWebSocket) {
422                 remoteControllerWebSocket.sendKeyPress(key);
423             }
424         } catch (RemoteControllerException e) {
425             reportError(String.format("Could not send command to device on %s:%d", host, port), e);
426         }
427     }
428
429     /**
430      * Sends a sequence of command to Samsung TV device.
431      *
432      * @param keys List of button codes to send
433      */
434     private void sendKeyCodes(final List<KeyCode> keys) {
435         try {
436             if (remoteController != null) {
437                 remoteController.sendKeys(keys);
438             }
439         } catch (RemoteControllerException e) {
440             reportError(String.format("Could not send command to device on %s:%d", host, port), e);
441         }
442     }
443
444     private void reportError(String message, RemoteControllerException e) {
445         reportError(ThingStatusDetail.COMMUNICATION_ERROR, message, e);
446     }
447
448     private void reportError(ThingStatusDetail statusDetail, String message, RemoteControllerException e) {
449         for (EventListener listener : listeners) {
450             listener.reportError(statusDetail, message, e);
451         }
452     }
453
454     @Override
455     public void appsUpdated(List<String> apps) {
456         // do nothing
457     }
458
459     @Override
460     public void currentAppUpdated(@Nullable String app) {
461         for (EventListener listener : listeners) {
462             listener.valueReceived(SOURCE_APP, new StringType(app));
463         }
464     }
465
466     @Override
467     public void powerUpdated(boolean on, boolean artmode) {
468         artModeSupported = true;
469         power = on;
470         this.artMode = artmode;
471
472         for (EventListener listener : listeners) {
473             // order of state updates is important to prevent extraneous transitions in overall state
474             if (on) {
475                 listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
476                 listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
477             } else {
478                 listener.valueReceived(ART_MODE, artmode ? OnOffType.ON : OnOffType.OFF);
479                 listener.valueReceived(POWER, on ? OnOffType.ON : OnOffType.OFF);
480             }
481         }
482     }
483
484     @Override
485     public void connectionError(@Nullable Throwable error) {
486         logger.debug("Connection error: {}", error != null ? error.getMessage() : "");
487         remoteController = null;
488     }
489
490     public boolean isArtModeSupported() {
491         return artModeSupported;
492     }
493
494     @Override
495     public void putConfig(String key, Object value) {
496         for (EventListener listener : listeners) {
497             listener.putConfig(key, value);
498         }
499     }
500
501     @Override
502     public @Nullable Object getConfig(String key) {
503         for (EventListener listener : listeners) {
504             return listener.getConfig(key);
505         }
506         return null;
507     }
508
509     @Override
510     public @Nullable WebSocketFactory getWebSocketFactory() {
511         for (EventListener listener : listeners) {
512             return listener.getWebSocketFactory();
513         }
514         return null;
515     }
516 }