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 static final 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 (IOException e) {
202 logger.debug("{}: Cannot connect to TV: {}", host, e.getMessage());
203 properties = Optional.empty();
204 } catch (JsonSyntaxException | URISyntaxException e) {
205 logger.warn("{}: Cannot connect to TV: {}", host, e.getMessage());
206 properties = Optional.empty();
213 * For Modern TVs get configuration, with time delay, and retry
215 * @param ms int delay in milliseconds
216 * @param retryCount int number of retries before giving up
217 * @return TVProperties
219 public TVProperties fetchTVProperties(int ms, int retryCount) {
220 ScheduledFuture<Optional<TVProperties>> future = scheduler.schedule(new FetchTVProperties(), ms,
221 TimeUnit.MILLISECONDS);
223 Optional<TVProperties> properties = future.get();
224 while (retryCount-- >= 0) {
225 if (properties.isPresent()) {
226 return properties.get();
227 } else if (retryCount > 0) {
228 logger.warn("{}: Cannot get TVProperties - Retry: {}", host, retryCount);
229 return fetchTVProperties(1000, retryCount);
232 } catch (InterruptedException | ExecutionException e) {
233 logger.warn("{}: Cannot get TVProperties: {}", host, e.getMessage());
235 logger.debug("{}: Cannot get TVProperties, return Empty properties", host);
236 return new TVProperties();
240 * Update WOL MAC address
241 * Discover the type of remote control service the TV supports.
242 * update artModeSupported and PowerState
243 * Update the configuration with results
246 private void discoverConfiguration() {
247 /* Check if configuration should be updated */
248 configuration = getConfigAs(SamsungTvConfiguration.class);
249 host = configuration.getHostName();
250 switch (configuration.getProtocol()) {
252 if (configuration.getMacAddress().isBlank()) {
253 String macAddress = WakeOnLanUtility.getMACAddress(host);
254 if (macAddress != null) {
255 putConfig(MAC_ADDRESS, macAddress);
258 TVProperties properties = fetchTVProperties(0, 0);
259 if ("Tizen".equals(properties.getOS())) {
260 if (properties.getTokenAuthSupport()) {
261 putConfig(PROTOCOL, PROTOCOL_SECUREWEBSOCKET);
262 putConfig(PORT, PORT_DEFAULT_SECUREWEBSOCKET);
264 putConfig(PROTOCOL, PROTOCOL_WEBSOCKET);
265 putConfig(PORT, PORT_DEFAULT_WEBSOCKET);
267 if ((configuration.getMacAddress().isBlank()) && !properties.getWifiMac().isBlank()) {
268 putConfig(MAC_ADDRESS, properties.getWifiMac());
270 updateSettings(properties);
275 for (int port : PORTS) {
277 RemoteControllerLegacy remoteController = new RemoteControllerLegacy(host, port, "openHAB",
279 remoteController.openConnection();
280 remoteController.close();
281 putConfig(PROTOCOL, SamsungTvConfiguration.PROTOCOL_LEGACY);
282 putConfig(PORT, port);
285 } catch (RemoteControllerException e) {
290 case PROTOCOL_WEBSOCKET:
291 case PROTOCOL_SECUREWEBSOCKET:
294 logger.warn("{}: TV binding is not yet Initialized", host);
297 case PROTOCOL_LEGACY:
304 public void initializeConfig() {
306 TVProperties properties = fetchTVProperties(0, 0);
307 if ("on".equals(properties.getPowerState())) {
308 updateSettings(properties);
313 public void updateSettings(TVProperties properties) {
314 setPowerState("on".equals(properties.getPowerState()));
315 setModelName(properties.getModelName());
316 int year = Integer.parseInt(properties.getModel().substring(0, 2));
317 if (properties.getFrameTVSupport() && year >= 22) {
318 logger.warn("{}: Art Mode MAY NOT BE SUPPORTED on Frame TV's after 2021 model year", host);
319 setArtMode2022(true);
322 setArtModeSupported(properties.getFrameTVSupport() && year < 22);
323 logger.debug("{}: Updated artModeSupported: {} PowerState: {}({}) artMode2022: {}", host, getArtModeSupported(),
324 getPowerState(), properties.getPowerState(), getArtMode2022());
328 public void showConfiguration() {
329 logger.debug("{}: Configuration: {}, port: {}, token: {}, MAC: {}, subscription: {}", host,
330 configuration.getProtocol(), configuration.getPort(), configuration.getWebsocketToken(),
331 configuration.getMacAddress(), configuration.getSubscription());
332 if (configuration.isWebsocketProtocol()) {
333 if (configuration.getSmartThingsApiKey().isBlank()) {
334 logger.debug("{}: SmartThings disabled", host);
336 logger.debug("{}: SmartThings enabled, device id: {}", host, configuration.getSmartThingsDeviceId());
342 * get PowerState from TVProperties
343 * Note: Series 7 TV's do not have the PowerState value
345 * @return String giving power state (TV can be on or standby, off if unreachable)
347 public String fetchPowerState() {
348 logger.trace("{}: fetching TV Power State", host);
349 TVProperties properties = fetchTVProperties(0, 2);
350 String powerState = properties.getPowerState();
351 setPowerState("on".equals(powerState));
352 logger.debug("{}: PowerState is: {}", host, powerState);
356 public boolean handleCommand(String channel, Command command, int ms) {
357 scheduler.schedule(() -> {
358 handleCommand(channel, command);
359 }, ms, TimeUnit.MILLISECONDS);
364 public void handleCommand(ChannelUID channelUID, Command command) {
365 logger.debug("{}: Received channel: {}, command: {}", host, channelUID, Utils.truncCmd(command));
366 handleCommand(channelUID.getId(), command);
369 public void handleCommand(String channel, Command command) {
370 logger.trace("{}: Received: {}, command: {}", host, channel, Utils.truncCmd(command));
372 // Delegate command to correct service
373 for (SamsungTvService service : services) {
374 for (String s : service.getSupportedChannelNames(command == RefreshType.REFRESH)) {
375 if (channel.equals(s)) {
376 if (service.handleCommand(channel, command)) {
382 // if power on/artmode on command try WOL if command failed:
383 if (!wolTask.send(channel, command)) {
384 if (getThing().getStatus() != ThingStatus.ONLINE) {
385 logger.warn("{}: TV is {}", host, getThing().getStatus());
387 logger.warn("{}: Channel '{}' not connected/supported", host, channel);
393 public void channelLinked(ChannelUID channelUID) {
394 logger.trace("{}: channelLinked: {}", host, channelUID);
395 if (POWER.equals(channelUID.getId())) {
396 valueReceived(POWER, OnOffType.from(getPowerState()));
398 services.stream().forEach(a -> a.clearCache());
399 if (Arrays.asList(ART_COLOR_TEMPERATURE, ART_IMAGE).contains(channelUID.getId())) {
400 // refresh channel as it's not polled
401 services.stream().filter(a -> a.getServiceName().equals(RemoteControllerService.SERVICE_NAME))
402 .map(a -> a.handleCommand(channelUID.getId(), RefreshType.REFRESH));
406 public void setModelName(String modelName) {
407 if (!modelName.isBlank()) {
408 this.modelName = modelName;
412 public String getModelName() {
416 public synchronized void setPowerState(boolean state) {
418 logger.trace("{}: PowerState set to: {}", host, powerState ? "on" : "off");
421 public boolean getPowerState() {
425 public void setArtMode2022(boolean artmode) {
426 artMode2022 = artmode;
429 public boolean getArtMode2022() {
433 public boolean getArtModeSupported() {
434 return artModeSupported;
437 public synchronized void setArtModeSupported(boolean artmode) {
438 if (!artModeSupported && artmode) {
439 logger.debug("{}: ArtMode Enabled", host);
441 artModeSupported = artmode;
445 public void initialize() {
446 updateStatus(ThingStatus.UNKNOWN);
448 logger.debug("{}: Initializing Samsung TV handler for uid '{}'", host, getThing().getUID());
449 if (host.isBlank()) {
450 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
451 "host ip address or name is blank");
455 // note this can take up to 500ms to return if TV is off
456 discoverConfiguration();
458 upnpService.getRegistry().addListener(this);
460 checkAndCreateServices();
464 * Start polling job with initial delay of 10 seconds if websocket protocol is selected
467 private void startPolling() {
468 int interval = configuration.getRefreshInterval();
469 int delay = configuration.isWebsocketProtocol() ? 10000 : 0;
470 if (pollingJob.map(job -> (job.isCancelled())).orElse(true)) {
471 logger.debug("{}: Start refresh task, interval={}", host, interval);
472 pollingJob = Optional
473 .of(scheduler.scheduleWithFixedDelay(this::poll, delay, interval, TimeUnit.MILLISECONDS));
477 private void stopPolling() {
478 pollingJob.ifPresent(job -> job.cancel(true));
479 pollingJob = Optional.empty();
483 public void dispose() {
484 logger.debug("{}: Disposing SamsungTvHandler", host);
487 setArtMode2022(false);
488 setArtModeSupported(false);
492 upnpService.getRegistry().removeListener(this);
495 private synchronized void stopServices() {
497 if (!services.isEmpty()) {
499 logger.debug("{}: Shutdown all Samsung services except RemoteControllerService", host);
500 services.stream().forEach(a -> stopService(a));
502 logger.debug("{}: Shutdown all Samsung services", host);
503 services.stream().forEach(a -> stopService(a));
509 private synchronized void shutdown() {
514 public synchronized void putOnline() {
515 if (getThing().getStatus() != ThingStatus.ONLINE) {
516 updateStatus(ThingStatus.ONLINE);
518 if (!getArtModeSupported()) {
519 if (getArtMode2022()) {
520 handleCommand(SET_ART_MODE, OnOffType.ON, 4000);
521 } else if (configuration.isWebsocketProtocol()) {
522 // if TV is registered to SmartThings it wakes up regularly (every 5 minutes or so), even if it's in
523 // standby, so check the power state locally to see if it's actually on
525 valueReceived(POWER, OnOffType.from(getPowerState()));
527 valueReceived(POWER, OnOffType.ON);
530 logger.debug("{}: TV is {}", host, getThing().getStatus());
534 public synchronized void putOffline() {
535 if (getThing().getStatus() != ThingStatus.OFFLINE) {
537 valueReceived(ART_MODE, OnOffType.OFF);
538 valueReceived(POWER, OnOffType.OFF);
539 if (getArtMode2022()) {
540 valueReceived(SET_ART_MODE, OnOffType.OFF);
542 valueReceived(ART_IMAGE, UnDefType.NULL);
543 valueReceived(ART_LABEL, new StringType(""));
544 valueReceived(SOURCE_APP, new StringType(""));
545 updateStatus(ThingStatus.OFFLINE);
546 logger.debug("{}: TV is {}", host, getThing().getStatus());
550 public boolean isChannelLinked(String ch) {
554 private boolean isDuplicateChannel(String channel) {
555 // Avoid redundant REFRESH commands when 2 channels are linked to the same action request
556 return (channel.equals(SOURCE_ID) && isLinked(SOURCE_NAME))
557 || (channel.equals(CHANNEL_NAME) && isLinked(PROGRAM_TITLE));
560 private void poll() {
562 // Skip channels if service is not connected/started
563 // Only poll SmartThings if TV is ON (ie playing)
564 services.stream().filter(service -> service.checkConnection()).filter(
565 service -> getPowerState() || !service.getServiceName().equals(SmartThingsApiService.SERVICE_NAME))
566 .forEach(service -> service.getSupportedChannelNames(true).stream()
567 .filter(channel -> isLinked(channel) && !isDuplicateChannel(channel))
568 .forEach(channel -> service.handleCommand(channel, RefreshType.REFRESH)));
569 } catch (Exception e) {
570 if (logger.isTraceEnabled()) {
571 logger.trace("{}: Polling Job exception: ", host, e);
573 logger.debug("{}: Polling Job exception: {}", host, e.getMessage());
578 public synchronized void valueReceived(String variable, State value) {
579 logger.debug("{}: Received value '{}':'{}' for thing '{}'", host, variable, value, this.getThing().getUID());
581 if (POWER.equals(variable)) {
582 setPowerState(OnOffType.ON.equals(value));
584 updateState(variable, value);
587 public void reportError(ThingStatusDetail statusDetail, @Nullable String message, @Nullable Throwable e) {
588 if (logger.isTraceEnabled()) {
589 logger.trace("{}: Error was reported: {}", host, message, e);
591 logger.debug("{}: Error was reported: {}, {}", host, message, (e != null) ? e.getMessage() : "");
593 updateStatus(ThingStatus.OFFLINE, statusDetail, message);
597 * One Samsung TV contains several UPnP devices. Samsung TV is discovered by
598 * Media Renderer UPnP device. This function tries to find another UPnP
599 * devices related to same Samsung TV and create handler for those.
600 * Also attempts to create websocket services if protocol is set to websocket
601 * And at least one UPNP service is discovered
602 * Smartthings service is also started if PAT (Api key) is entered
604 private void checkAndCreateServices() {
605 logger.debug("{}: Check and create missing services", host);
607 boolean isOnline = false;
610 for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
611 RemoteDevice rdevice = (RemoteDevice) device;
612 if (host.equals(Utils.getHost(rdevice))) {
613 setModelName(Utils.getModelName(rdevice));
614 isOnline = createService(Utils.getType(rdevice), Utils.getUdn(rdevice)) || isOnline;
618 // Websocket services and Smartthings service
619 if ((isOnline | getArtMode2022()) && configuration.isWebsocketProtocol()) {
620 createService(RemoteControllerService.SERVICE_NAME, "");
621 if (!configuration.getSmartThingsApiKey().isBlank()) {
622 createService(SmartThingsApiService.SERVICE_NAME, "");
634 * Create or restart existing Samsung TV service.
635 * udn is used to determine whether to start upnp service or websocket
640 * @return true if service restated or created, false otherwise
642 private synchronized boolean createService(String type, String udn) {
643 Optional<SamsungTvService> service = findServiceInstance(type);
645 if (service.isPresent()) {
646 if ((!udn.isBlank() && service.get().isUpnp()) || (udn.isBlank() && !service.get().isUpnp())) {
647 logger.debug("{}: Service rediscovered, clearing caches: {}, {} ({})", host, getModelName(), type, udn);
648 service.get().clearCache();
654 service = createNewService(type, udn);
655 if (service.isPresent()) {
656 startService(service.get());
657 logger.debug("{}: Started service for: {}, {} ({})", host, getModelName(), type, udn);
660 logger.trace("{}: Skipping unknown service: {}, {} ({})", host, modelName, type, udn);
665 * Create Samsung TV service.
666 * udn is used to determine whether to start upnp service or websocket
670 * @return service or null
672 private synchronized Optional<SamsungTvService> createNewService(String type, String udn) {
673 Optional<SamsungTvService> service = Optional.empty();
676 case MainTVServerService.SERVICE_NAME:
677 service = Optional.of(new MainTVServerService(upnpIOService, udn, host, this));
679 case MediaRendererService.SERVICE_NAME:
680 service = Optional.of(new MediaRendererService(upnpIOService, udn, host, this));
682 case RemoteControllerService.SERVICE_NAME:
684 if (configuration.isWebsocketProtocol() && !udn.isEmpty()) {
685 throw new RemoteControllerException("config is websocket - ignoring UPNP service");
688 .of(new RemoteControllerService(host, configuration.getPort(), !udn.isEmpty(), this));
689 } catch (RemoteControllerException e) {
690 logger.warn("{}: Not creating remote controller service: {}", host, e.getMessage());
693 case SmartThingsApiService.SERVICE_NAME:
694 service = Optional.of(new SmartThingsApiService(host, this));
700 public synchronized Optional<SamsungTvService> findServiceInstance(String serviceName) {
701 return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst();
704 private synchronized void startService(SamsungTvService service) {
706 services.add(service);
709 private synchronized void stopService(SamsungTvService service) {
710 if (isFrame2022() && service.getServiceName().equals(RemoteControllerService.SERVICE_NAME)) {
711 // don't stop the remoteController service on 2022 frame TV's
712 logger.debug("{}: not stopping: {}", host, service.getServiceName());
716 services.remove(service);
720 public void remoteDeviceAdded(@Nullable Registry registry, @Nullable RemoteDevice device) {
721 if (device != null && host.equals(Utils.getHost(device))) {
722 logger.debug("{}: remoteDeviceAdded: {}, {}, upnpUDN={}", host, Utils.getType(device),
723 device.getIdentity().getDescriptorURL(), Utils.getUdn(device));
725 checkAndCreateServices();
730 public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
731 if (device != null && host.equals(Utils.getHost(device))) {
732 if (services.stream().anyMatch(s -> s.getServiceName().equals(Utils.getType(device)))) {
733 logger.debug("{}: Device removed: {}, udn={}", host, Utils.getType(device), Utils.getUdn(device));
740 public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
744 public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
748 public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
749 @Nullable Exception ex) {
753 public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
757 public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
761 public void beforeShutdown(@Nullable Registry registry) {
765 public void afterShutdown() {
768 public boolean isFrame2022() {
769 return getArtMode2022() || (getArtModeSupported() && artApiVersion >= 1);
772 public void setOffline() {
773 // schedule this in the future to allow calling service to return immediately
774 scheduler.submit(this::shutdown);
777 public void putConfig(@Nullable String key, @Nullable Object value) {
778 if (key != null && value != null) {
779 getConfig().put(key, value);
780 Configuration config = editConfiguration();
781 config.put(key, value);
782 updateConfiguration(config);
783 logger.debug("{}: Updated Configuration {}:{}", host, key, value);
784 configuration = getConfigAs(SamsungTvConfiguration.class);
788 public ScheduledExecutorService getScheduler() {
792 public WebSocketFactory getWebSocketFactory() {
793 return webSocketFactory;