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.handler;
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
16 import static org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration.*;
18 import java.io.IOException;
20 import java.net.URISyntaxException;
21 import java.util.Arrays;
22 import java.util.List;
23 import java.util.Optional;
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.CopyOnWriteArraySet;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ScheduledExecutorService;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.jupnp.UpnpService;
35 import org.jupnp.model.meta.Device;
36 import org.jupnp.model.meta.LocalDevice;
37 import org.jupnp.model.meta.RemoteDevice;
38 import org.jupnp.registry.Registry;
39 import org.jupnp.registry.RegistryListener;
40 import org.openhab.binding.samsungtv.internal.Utils;
41 import org.openhab.binding.samsungtv.internal.WakeOnLanUtility;
42 import org.openhab.binding.samsungtv.internal.WolSend;
43 import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
44 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerException;
45 import org.openhab.binding.samsungtv.internal.protocol.RemoteControllerLegacy;
46 import org.openhab.binding.samsungtv.internal.service.MainTVServerService;
47 import org.openhab.binding.samsungtv.internal.service.MediaRendererService;
48 import org.openhab.binding.samsungtv.internal.service.RemoteControllerService;
49 import org.openhab.binding.samsungtv.internal.service.SmartThingsApiService;
50 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
51 import org.openhab.core.config.core.Configuration;
52 import org.openhab.core.io.net.http.HttpUtil;
53 import org.openhab.core.io.net.http.WebSocketFactory;
54 import org.openhab.core.io.transport.upnp.UpnpIOService;
55 import org.openhab.core.library.types.OnOffType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.Thing;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.binding.BaseThingHandler;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
69 import com.google.gson.Gson;
70 import com.google.gson.JsonSyntaxException;
73 * The {@link SamsungTvHandler} is responsible for handling commands, which are
74 * sent to one of the channels.
76 * @author Pauli Anttila - Initial contribution
77 * @author Martin van Wingerden - Some changes for non-UPnP configured devices
78 * @author Arjan Mels - Remove RegistryListener, manually create RemoteService in all circumstances, add sending of WOL
79 * package to power on TV
80 * @author Nick Waterton - Improve Frame TV handling and some refactoring
83 public class SamsungTvHandler extends BaseThingHandler implements RegistryListener {
85 /** Path for the information endpoint (note the final slash!) */
86 private static final String HTTP_ENDPOINT_V2 = "/api/v2/";
88 // common Samsung TV remote control ports
89 private final static List<Integer> PORTS = List.of(55000, 1515, 7001, 15500);
91 private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
93 private final UpnpIOService upnpIOService;
94 private final UpnpService upnpService;
95 private final WebSocketFactory webSocketFactory;
97 public SamsungTvConfiguration configuration;
99 public String host = "";
100 private String modelName = "";
101 public int artApiVersion = 0;
103 /* Samsung TV services */
104 private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
106 /* Store powerState to be able to restore upon new link */
107 private boolean powerState = false;
109 /* Store if art mode is supported to be able to skip switching power state to ON during initialization */
110 public boolean artModeSupported = false;
111 /* Art Mode on TV's >= 2022 is not properly supported - need workarounds for power */
112 public boolean artMode2022 = false;
113 /* Is binding initialized? */
114 public boolean initialized = false;
116 private Optional<ScheduledFuture<?>> pollingJob = Optional.empty();
117 private WolSend wolTask = new WolSend(this);
119 /** Description of the json returned for the information endpoint */
120 @NonNullByDefault({})
121 public class TVProperties {
123 boolean FrameTVSupport;
124 boolean GamePadSupport;
125 boolean ImeSyncedSupport;
128 boolean TokenAuthSupport;
129 boolean VoiceSupport;
132 String firmwareVersion;
145 public boolean getFrameTVSupport() {
146 return Optional.ofNullable(device).map(a -> a.FrameTVSupport).orElse(false);
149 public boolean getTokenAuthSupport() {
150 return Optional.ofNullable(device).map(a -> a.TokenAuthSupport).orElse(false);
153 public String getPowerState() {
154 if (!getOS().isBlank()) {
155 return Optional.ofNullable(device).map(a -> a.PowerState).orElse("on");
160 public String getOS() {
161 return Optional.ofNullable(device).map(a -> a.OS).orElse("");
164 public String getWifiMac() {
165 return Optional.ofNullable(device).map(a -> a.wifiMac).filter(m -> m.length() == 17).orElse("");
168 public String getModel() {
169 return Optional.ofNullable(device).map(a -> a.model).orElse("");
172 public String getModelName() {
173 return Optional.ofNullable(device).map(a -> a.modelName).orElse("");
177 public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
178 WebSocketFactory webSocketFactory) {
180 this.upnpIOService = upnpIOService;
181 this.upnpService = upnpService;
182 this.webSocketFactory = webSocketFactory;
183 this.configuration = getConfigAs(SamsungTvConfiguration.class);
184 this.host = configuration.getHostName();
185 logger.debug("{}: Create a Samsung TV Handler for thing '{}'", host, getThing().getUID());
189 * For Modern TVs get configuration, with 500 ms timeout
192 public class FetchTVProperties implements Callable<Optional<TVProperties>> {
193 public Optional<TVProperties> call() throws Exception {
194 logger.trace("{}: getting TV properties", host);
195 Optional<TVProperties> properties = Optional.empty();
197 URI uri = new URI("http", null, host, PORT_DEFAULT_WEBSOCKET, HTTP_ENDPOINT_V2, null, null);
199 String response = HttpUtil.executeUrl("GET", uri.toURL().toString(), 500);
200 properties = Optional.ofNullable(new Gson().fromJson(response, TVProperties.class));
201 } catch (JsonSyntaxException | URISyntaxException | IOException e) {
202 logger.warn("{}: Cannot connect to TV: {}", host, e.getMessage());
203 properties = Optional.empty();
210 * For Modern TVs get configuration, with time delay, and retry
212 * @param ms int delay in milliseconds
213 * @param retryCount int number of retries before giving up
214 * @return TVProperties
216 public TVProperties fetchTVProperties(int ms, int retryCount) {
217 ScheduledFuture<Optional<TVProperties>> future = scheduler.schedule(new FetchTVProperties(), ms,
218 TimeUnit.MILLISECONDS);
220 Optional<TVProperties> properties = future.get();
221 while (retryCount-- >= 0) {
222 if (properties.isPresent()) {
223 return properties.get();
224 } else if (retryCount > 0) {
225 logger.warn("{}: Cannot get TVProperties - Retry: {}", host, retryCount);
226 return fetchTVProperties(1000, retryCount);
229 } catch (InterruptedException | ExecutionException e) {
230 logger.warn("{}: Cannot get TVProperties: {}", host, e.getMessage());
232 logger.warn("{}: Cannot get TVProperties, return Empty properties", host);
233 return new TVProperties();
237 * Update WOL MAC address
238 * Discover the type of remote control service the TV supports.
239 * update artModeSupported and PowerState
240 * Update the configuration with results
243 private void discoverConfiguration() {
244 /* Check if configuration should be updated */
245 configuration = getConfigAs(SamsungTvConfiguration.class);
246 host = configuration.getHostName();
247 switch (configuration.getProtocol()) {
249 if (configuration.getMacAddress().isBlank()) {
250 String macAddress = WakeOnLanUtility.getMACAddress(host);
251 if (macAddress != null) {
252 putConfig(MAC_ADDRESS, macAddress);
255 TVProperties properties = fetchTVProperties(0, 0);
256 if ("Tizen".equals(properties.getOS())) {
257 if (properties.getTokenAuthSupport()) {
258 putConfig(PROTOCOL, PROTOCOL_SECUREWEBSOCKET);
259 putConfig(PORT, PORT_DEFAULT_SECUREWEBSOCKET);
261 putConfig(PROTOCOL, PROTOCOL_WEBSOCKET);
262 putConfig(PORT, PORT_DEFAULT_WEBSOCKET);
264 if ((configuration.getMacAddress().isBlank()) && !properties.getWifiMac().isBlank()) {
265 putConfig(MAC_ADDRESS, properties.getWifiMac());
267 updateSettings(properties);
272 for (int port : PORTS) {
274 RemoteControllerLegacy remoteController = new RemoteControllerLegacy(host, port, "openHAB",
276 remoteController.openConnection();
277 remoteController.close();
278 putConfig(PROTOCOL, SamsungTvConfiguration.PROTOCOL_LEGACY);
279 putConfig(PORT, port);
282 } catch (RemoteControllerException e) {
287 case PROTOCOL_WEBSOCKET:
288 case PROTOCOL_SECUREWEBSOCKET:
291 logger.warn("{}: TV binding is not yet Initialized", host);
294 case PROTOCOL_LEGACY:
301 public void initializeConfig() {
303 TVProperties properties = fetchTVProperties(0, 0);
304 if ("on".equals(properties.getPowerState())) {
305 updateSettings(properties);
310 public void updateSettings(TVProperties properties) {
311 setPowerState("on".equals(properties.getPowerState()));
312 setModelName(properties.getModelName());
313 int year = Integer.parseInt(properties.getModel().substring(0, 2));
314 if (properties.getFrameTVSupport() && year >= 22) {
315 logger.warn("{}: Art Mode MAY NOT BE SUPPORTED on Frame TV's after 2021 model year", host);
316 setArtMode2022(true);
319 setArtModeSupported(properties.getFrameTVSupport() && year < 22);
320 logger.debug("{}: Updated artModeSupported: {} PowerState: {}({}) artMode2022: {}", host, getArtModeSupported(),
321 getPowerState(), properties.getPowerState(), getArtMode2022());
325 public void showConfiguration() {
326 logger.debug("{}: Configuration: {}, port: {}, token: {}, MAC: {}, subscription: {}", host,
327 configuration.getProtocol(), configuration.getPort(), configuration.getWebsocketToken(),
328 configuration.getMacAddress(), configuration.getSubscription());
329 if (configuration.isWebsocketProtocol()) {
330 if (configuration.getSmartThingsApiKey().isBlank()) {
331 logger.debug("{}: SmartThings disabled", host);
333 logger.debug("{}: SmartThings enabled, device id: {}", host, configuration.getSmartThingsDeviceId());
339 * get PowerState from TVProperties
340 * Note: Series 7 TV's do not have the PowerState value
342 * @return String giving power state (TV can be on or standby, off if unreachable)
344 public String fetchPowerState() {
345 logger.trace("{}: fetching TV Power State", host);
346 TVProperties properties = fetchTVProperties(0, 2);
347 String PowerState = properties.getPowerState();
348 setPowerState("on".equals(PowerState));
349 logger.debug("{}: PowerState is: {}", host, PowerState);
353 public boolean handleCommand(String channel, Command command, int ms) {
354 scheduler.schedule(() -> {
355 handleCommand(channel, command);
356 }, ms, TimeUnit.MILLISECONDS);
361 public void handleCommand(ChannelUID channelUID, Command command) {
362 logger.debug("{}: Received channel: {}, command: {}", host, channelUID, Utils.truncCmd(command));
363 handleCommand(channelUID.getId(), command);
366 public void handleCommand(String channel, Command command) {
367 logger.trace("{}: Received: {}, command: {}", host, channel, Utils.truncCmd(command));
369 // Delegate command to correct service
370 for (SamsungTvService service : services) {
371 for (String s : service.getSupportedChannelNames(command == RefreshType.REFRESH)) {
372 if (channel.equals(s)) {
373 if (service.handleCommand(channel, command)) {
379 // if power on/artmode on command try WOL if command failed:
380 if (!wolTask.send(channel, command)) {
381 if (getThing().getStatus() != ThingStatus.ONLINE) {
382 logger.warn("{}: TV is {}", host, getThing().getStatus());
384 logger.warn("{}: Channel '{}' not connected/supported", host, channel);
390 public void channelLinked(ChannelUID channelUID) {
391 logger.trace("{}: channelLinked: {}", host, channelUID);
392 if (POWER.equals(channelUID.getId())) {
393 valueReceived(POWER, OnOffType.from(getPowerState()));
395 services.stream().forEach(a -> a.clearCache());
396 if (Arrays.asList(ART_COLOR_TEMPERATURE, ART_IMAGE).contains(channelUID.getId())) {
397 // refresh channel as it's not polled
398 services.stream().filter(a -> a.getServiceName().equals(RemoteControllerService.SERVICE_NAME))
399 .map(a -> a.handleCommand(channelUID.getId(), RefreshType.REFRESH));
403 public void setModelName(String modelName) {
404 if (!modelName.isBlank()) {
405 this.modelName = modelName;
409 public String getModelName() {
413 public synchronized void setPowerState(boolean state) {
415 logger.trace("{}: PowerState set to: {}", host, powerState ? "on" : "off");
418 public boolean getPowerState() {
422 public void setArtMode2022(boolean artmode) {
423 artMode2022 = artmode;
426 public boolean getArtMode2022() {
430 public boolean getArtModeSupported() {
431 return artModeSupported;
434 public synchronized void setArtModeSupported(boolean artmode) {
435 if (!artModeSupported && artmode) {
436 logger.debug("{}: ArtMode Enabled", host);
438 artModeSupported = artmode;
442 public void initialize() {
443 updateStatus(ThingStatus.UNKNOWN);
445 logger.debug("{}: Initializing Samsung TV handler for uid '{}'", host, getThing().getUID());
446 if (host.isBlank()) {
447 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
448 "host ip address or name is blank");
452 // note this can take up to 500ms to return if TV is off
453 discoverConfiguration();
455 upnpService.getRegistry().addListener(this);
457 checkAndCreateServices();
461 * Start polling job with initial delay of 10 seconds if websocket protocol is selected
464 private void startPolling() {
465 int interval = configuration.getRefreshInterval();
466 int delay = configuration.isWebsocketProtocol() ? 10000 : 0;
467 if (pollingJob.map(job -> (job.isCancelled())).orElse(true)) {
468 logger.debug("{}: Start refresh task, interval={}", host, interval);
469 pollingJob = Optional
470 .of(scheduler.scheduleWithFixedDelay(this::poll, delay, interval, TimeUnit.MILLISECONDS));
474 private void stopPolling() {
475 pollingJob.ifPresent(job -> job.cancel(true));
476 pollingJob = Optional.empty();
480 public void dispose() {
481 logger.debug("{}: Disposing SamsungTvHandler", host);
484 setArtMode2022(false);
485 setArtModeSupported(false);
489 upnpService.getRegistry().removeListener(this);
492 private synchronized void stopServices() {
494 if (!services.isEmpty()) {
496 logger.debug("{}: Shutdown all Samsung services except RemoteControllerService", host);
497 services.stream().forEach(a -> stopService(a));
499 logger.debug("{}: Shutdown all Samsung services", host);
500 services.stream().forEach(a -> stopService(a));
506 private synchronized void shutdown() {
511 public synchronized void putOnline() {
512 if (getThing().getStatus() != ThingStatus.ONLINE) {
513 updateStatus(ThingStatus.ONLINE);
515 if (!getArtModeSupported()) {
516 if (getArtMode2022()) {
517 handleCommand(SET_ART_MODE, OnOffType.ON, 4000);
518 } else if (configuration.isWebsocketProtocol()) {
519 // if TV is registered to SmartThings it wakes up regularly (every 5 minutes or so), even if it's in
520 // standby, so check the power state locally to see if it's actually on
522 valueReceived(POWER, OnOffType.from(getPowerState()));
524 valueReceived(POWER, OnOffType.ON);
527 logger.debug("{}: TV is {}", host, getThing().getStatus());
531 public synchronized void putOffline() {
532 if (getThing().getStatus() != ThingStatus.OFFLINE) {
534 valueReceived(ART_MODE, OnOffType.OFF);
535 valueReceived(POWER, OnOffType.OFF);
536 if (getArtMode2022()) {
537 valueReceived(SET_ART_MODE, OnOffType.OFF);
539 valueReceived(ART_IMAGE, UnDefType.NULL);
540 valueReceived(ART_LABEL, new StringType(""));
541 valueReceived(SOURCE_APP, new StringType(""));
542 updateStatus(ThingStatus.OFFLINE);
543 logger.debug("{}: TV is {}", host, getThing().getStatus());
547 public boolean isChannelLinked(String ch) {
551 private boolean isDuplicateChannel(String channel) {
552 // Avoid redundant REFRESH commands when 2 channels are linked to the same action request
553 return (channel.equals(SOURCE_ID) && isLinked(SOURCE_NAME))
554 || (channel.equals(CHANNEL_NAME) && isLinked(PROGRAM_TITLE));
557 private void poll() {
559 // Skip channels if service is not connected/started
560 services.stream().filter(service -> service.checkConnection())
561 .forEach(service -> service.getSupportedChannelNames(true).stream()
562 .filter(channel -> isLinked(channel) && !isDuplicateChannel(channel))
563 .forEach(channel -> service.handleCommand(channel, RefreshType.REFRESH)));
564 } catch (Exception e) {
565 if (logger.isTraceEnabled()) {
566 logger.trace("{}: Polling Job exception: ", host, e);
568 logger.debug("{}: Polling Job exception: {}", host, e.getMessage());
573 public synchronized void valueReceived(String variable, State value) {
574 logger.debug("{}: Received value '{}':'{}' for thing '{}'", host, variable, value, this.getThing().getUID());
576 if (POWER.equals(variable)) {
577 setPowerState(OnOffType.ON.equals(value));
579 updateState(variable, value);
582 public void reportError(ThingStatusDetail statusDetail, @Nullable String message, @Nullable Throwable e) {
583 if (logger.isTraceEnabled()) {
584 logger.trace("{}: Error was reported: {}", host, message, e);
586 logger.debug("{}: Error was reported: {}, {}", host, message, (e != null) ? e.getMessage() : "");
588 updateStatus(ThingStatus.OFFLINE, statusDetail, message);
592 * One Samsung TV contains several UPnP devices. Samsung TV is discovered by
593 * Media Renderer UPnP device. This function tries to find another UPnP
594 * devices related to same Samsung TV and create handler for those.
595 * Also attempts to create websocket services if protocol is set to websocket
596 * And at least one UPNP service is discovered
597 * Smartthings service is also started if PAT (Api key) is entered
599 private void checkAndCreateServices() {
600 logger.debug("{}: Check and create missing services", host);
602 boolean isOnline = false;
605 for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
606 RemoteDevice rdevice = (RemoteDevice) device;
607 if (host.equals(Utils.getHost(rdevice))) {
608 setModelName(Utils.getModelName(rdevice));
609 isOnline = createService(Utils.getType(rdevice), Utils.getUdn(rdevice)) || isOnline;
613 // Websocket services and Smartthings service
614 if ((isOnline | getArtMode2022()) && configuration.isWebsocketProtocol()) {
615 createService(RemoteControllerService.SERVICE_NAME, "");
616 if (!configuration.getSmartThingsApiKey().isBlank()) {
617 createService(SmartThingsApiService.SERVICE_NAME, "");
629 * Create or restart existing Samsung TV service.
630 * udn is used to determine whether to start upnp service or websocket
635 * @return true if service restated or created, false otherwise
637 private synchronized boolean createService(String type, String udn) {
639 Optional<SamsungTvService> service = findServiceInstance(type);
641 if (service.isPresent()) {
642 if ((!udn.isBlank() && service.get().isUpnp()) || (udn.isBlank() && !service.get().isUpnp())) {
643 logger.debug("{}: Service rediscovered, clearing caches: {}, {} ({})", host, getModelName(), type, udn);
644 service.get().clearCache();
650 service = createNewService(type, udn);
651 if (service.isPresent()) {
652 startService(service.get());
653 logger.debug("{}: Started service for: {}, {} ({})", host, getModelName(), type, udn);
656 logger.trace("{}: Skipping unknown service: {}, {} ({})", host, modelName, type, udn);
661 * Create Samsung TV service.
662 * udn is used to determine whether to start upnp service or websocket
666 * @return service or null
668 private synchronized Optional<SamsungTvService> createNewService(String type, String udn) {
669 Optional<SamsungTvService> service = Optional.empty();
672 case MainTVServerService.SERVICE_NAME:
673 service = Optional.of(new MainTVServerService(upnpIOService, udn, host, this));
675 case MediaRendererService.SERVICE_NAME:
676 service = Optional.of(new MediaRendererService(upnpIOService, udn, host, this));
678 case RemoteControllerService.SERVICE_NAME:
680 if (configuration.isWebsocketProtocol() && !udn.isEmpty()) {
681 throw new RemoteControllerException("config is websocket - ignoring UPNP service");
684 .of(new RemoteControllerService(host, configuration.getPort(), !udn.isEmpty(), this));
685 } catch (RemoteControllerException e) {
686 logger.warn("{}: Not creating remote controller service: {}", host, e.getMessage());
689 case SmartThingsApiService.SERVICE_NAME:
690 service = Optional.of(new SmartThingsApiService(host, this));
696 public synchronized Optional<SamsungTvService> findServiceInstance(String serviceName) {
697 return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst();
700 private synchronized void startService(SamsungTvService service) {
702 services.add(service);
705 private synchronized void stopService(SamsungTvService service) {
706 if (isFrame2022() && service.getServiceName().equals(RemoteControllerService.SERVICE_NAME)) {
707 // don't stop the remoteController service on 2022 frame TV's
708 logger.debug("{}: not stopping: {}", host, service.getServiceName());
712 services.remove(service);
716 public void remoteDeviceAdded(@Nullable Registry registry, @Nullable RemoteDevice device) {
717 if (device != null && host.equals(Utils.getHost(device))) {
718 logger.debug("{}: remoteDeviceAdded: {}, {}, upnpUDN={}", host, Utils.getType(device),
719 device.getIdentity().getDescriptorURL(), Utils.getUdn(device));
721 checkAndCreateServices();
726 public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
727 if (device != null && host.equals(Utils.getHost(device))) {
728 if (services.stream().anyMatch(s -> s.getServiceName().equals(Utils.getType(device)))) {
729 logger.debug("{}: Device removed: {}, udn={}", host, Utils.getType(device), Utils.getUdn(device));
736 public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
740 public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
744 public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
745 @Nullable Exception ex) {
749 public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
753 public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
757 public void beforeShutdown(@Nullable Registry registry) {
761 public void afterShutdown() {
764 public boolean isFrame2022() {
765 return getArtMode2022() || (getArtModeSupported() && artApiVersion >= 1);
768 public void setOffline() {
769 // schedule this in the future to allow calling service to return immediately
770 scheduler.submit(this::shutdown);
773 public void putConfig(@Nullable String key, @Nullable Object value) {
774 if (key != null && value != null) {
775 getConfig().put(key, value);
776 Configuration config = editConfiguration();
777 config.put(key, value);
778 updateConfiguration(config);
779 logger.debug("{}: Updated Configuration {}:{}", host, key, value);
780 configuration = getConfigAs(SamsungTvConfiguration.class);
784 public ScheduledExecutorService getScheduler() {
788 public WebSocketFactory getWebSocketFactory() {
789 return webSocketFactory;