]> git.basschouten.com Git - openhab-addons.git/blob
c94c3c9f622a97f3275afafdf9748b2e14707a0b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.handler;
14
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
16 import static org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration.*;
17
18 import java.io.IOException;
19 import java.net.URI;
20 import java.net.URISyntaxException;
21 import java.util.Arrays;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.Set;
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;
31
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;
68
69 import com.google.gson.Gson;
70 import com.google.gson.JsonSyntaxException;
71
72 /**
73  * The {@link SamsungTvHandler} is responsible for handling commands, which are
74  * sent to one of the channels.
75  *
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
81  */
82 @NonNullByDefault
83 public class SamsungTvHandler extends BaseThingHandler implements RegistryListener {
84
85     /** Path for the information endpoint (note the final slash!) */
86     private static final String HTTP_ENDPOINT_V2 = "/api/v2/";
87
88     // common Samsung TV remote control ports
89     private final static List<Integer> PORTS = List.of(55000, 1515, 7001, 15500);
90
91     private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
92
93     private final UpnpIOService upnpIOService;
94     private final UpnpService upnpService;
95     private final WebSocketFactory webSocketFactory;
96
97     public SamsungTvConfiguration configuration;
98
99     public String host = "";
100     private String modelName = "";
101     public int artApiVersion = 0;
102
103     /* Samsung TV services */
104     private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
105
106     /* Store powerState to be able to restore upon new link */
107     private boolean powerState = false;
108
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;
115
116     private Optional<ScheduledFuture<?>> pollingJob = Optional.empty();
117     private WolSend wolTask = new WolSend(this);
118
119     /** Description of the json returned for the information endpoint */
120     @NonNullByDefault({})
121     public class TVProperties {
122         class Device {
123             boolean FrameTVSupport;
124             boolean GamePadSupport;
125             boolean ImeSyncedSupport;
126             String OS;
127             String PowerState;
128             boolean TokenAuthSupport;
129             boolean VoiceSupport;
130             String countryCode;
131             String description;
132             String firmwareVersion;
133             String model;
134             String modelName;
135             String name;
136             String networkType;
137             String resolution;
138             String id;
139             String wifiMac;
140         }
141
142         Device device;
143         String isSupport;
144
145         public boolean getFrameTVSupport() {
146             return Optional.ofNullable(device).map(a -> a.FrameTVSupport).orElse(false);
147         }
148
149         public boolean getTokenAuthSupport() {
150             return Optional.ofNullable(device).map(a -> a.TokenAuthSupport).orElse(false);
151         }
152
153         public String getPowerState() {
154             if (!getOS().isBlank()) {
155                 return Optional.ofNullable(device).map(a -> a.PowerState).orElse("on");
156             }
157             return "off";
158         }
159
160         public String getOS() {
161             return Optional.ofNullable(device).map(a -> a.OS).orElse("");
162         }
163
164         public String getWifiMac() {
165             return Optional.ofNullable(device).map(a -> a.wifiMac).filter(m -> m.length() == 17).orElse("");
166         }
167
168         public String getModel() {
169             return Optional.ofNullable(device).map(a -> a.model).orElse("");
170         }
171
172         public String getModelName() {
173             return Optional.ofNullable(device).map(a -> a.modelName).orElse("");
174         }
175     }
176
177     public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
178             WebSocketFactory webSocketFactory) {
179         super(thing);
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());
186     }
187
188     /**
189      * For Modern TVs get configuration, with 500 ms timeout
190      *
191      */
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();
196             try {
197                 URI uri = new URI("http", null, host, PORT_DEFAULT_WEBSOCKET, HTTP_ENDPOINT_V2, null, null);
198                 // @Nullable
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();
204             }
205             return properties;
206         }
207     }
208
209     /**
210      * For Modern TVs get configuration, with time delay, and retry
211      *
212      * @param ms int delay in milliseconds
213      * @param retryCount int number of retries before giving up
214      * @return TVProperties
215      */
216     public TVProperties fetchTVProperties(int ms, int retryCount) {
217         ScheduledFuture<Optional<TVProperties>> future = scheduler.schedule(new FetchTVProperties(), ms,
218                 TimeUnit.MILLISECONDS);
219         try {
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);
227                 }
228             }
229         } catch (InterruptedException | ExecutionException e) {
230             logger.warn("{}: Cannot get TVProperties: {}", host, e.getMessage());
231         }
232         logger.warn("{}: Cannot get TVProperties, return Empty properties", host);
233         return new TVProperties();
234     }
235
236     /**
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
241      *
242      */
243     private void discoverConfiguration() {
244         /* Check if configuration should be updated */
245         configuration = getConfigAs(SamsungTvConfiguration.class);
246         host = configuration.getHostName();
247         switch (configuration.getProtocol()) {
248             case PROTOCOL_NONE:
249                 if (configuration.getMacAddress().isBlank()) {
250                     String macAddress = WakeOnLanUtility.getMACAddress(host);
251                     if (macAddress != null) {
252                         putConfig(MAC_ADDRESS, macAddress);
253                     }
254                 }
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);
260                     } else {
261                         putConfig(PROTOCOL, PROTOCOL_WEBSOCKET);
262                         putConfig(PORT, PORT_DEFAULT_WEBSOCKET);
263                     }
264                     if ((configuration.getMacAddress().isBlank()) && !properties.getWifiMac().isBlank()) {
265                         putConfig(MAC_ADDRESS, properties.getWifiMac());
266                     }
267                     updateSettings(properties);
268                     break;
269                 }
270
271                 initialized = true;
272                 for (int port : PORTS) {
273                     try {
274                         RemoteControllerLegacy remoteController = new RemoteControllerLegacy(host, port, "openHAB",
275                                 "openHAB");
276                         remoteController.openConnection();
277                         remoteController.close();
278                         putConfig(PROTOCOL, SamsungTvConfiguration.PROTOCOL_LEGACY);
279                         putConfig(PORT, port);
280                         setPowerState(true);
281                         break;
282                     } catch (RemoteControllerException e) {
283                         // ignore error
284                     }
285                 }
286                 break;
287             case PROTOCOL_WEBSOCKET:
288             case PROTOCOL_SECUREWEBSOCKET:
289                 initializeConfig();
290                 if (!initialized) {
291                     logger.warn("{}: TV binding is not yet Initialized", host);
292                 }
293                 break;
294             case PROTOCOL_LEGACY:
295                 initialized = true;
296                 break;
297         }
298         showConfiguration();
299     }
300
301     public void initializeConfig() {
302         if (!initialized) {
303             TVProperties properties = fetchTVProperties(0, 0);
304             if ("on".equals(properties.getPowerState())) {
305                 updateSettings(properties);
306             }
307         }
308     }
309
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);
317             artApiVersion = 1;
318         }
319         setArtModeSupported(properties.getFrameTVSupport() && year < 22);
320         logger.debug("{}: Updated artModeSupported: {} PowerState: {}({}) artMode2022: {}", host, getArtModeSupported(),
321                 getPowerState(), properties.getPowerState(), getArtMode2022());
322         initialized = true;
323     }
324
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);
332             } else {
333                 logger.debug("{}: SmartThings enabled, device id: {}", host, configuration.getSmartThingsDeviceId());
334             }
335         }
336     }
337
338     /**
339      * get PowerState from TVProperties
340      * Note: Series 7 TV's do not have the PowerState value
341      *
342      * @return String giving power state (TV can be on or standby, off if unreachable)
343      */
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);
350         return PowerState;
351     }
352
353     public boolean handleCommand(String channel, Command command, int ms) {
354         scheduler.schedule(() -> {
355             handleCommand(channel, command);
356         }, ms, TimeUnit.MILLISECONDS);
357         return true;
358     }
359
360     @Override
361     public void handleCommand(ChannelUID channelUID, Command command) {
362         logger.debug("{}: Received channel: {}, command: {}", host, channelUID, Utils.truncCmd(command));
363         handleCommand(channelUID.getId(), command);
364     }
365
366     public void handleCommand(String channel, Command command) {
367         logger.trace("{}: Received: {}, command: {}", host, channel, Utils.truncCmd(command));
368
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)) {
374                         return;
375                     }
376                 }
377             }
378         }
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());
383             } else {
384                 logger.warn("{}: Channel '{}' not connected/supported", host, channel);
385             }
386         }
387     }
388
389     @Override
390     public void channelLinked(ChannelUID channelUID) {
391         logger.trace("{}: channelLinked: {}", host, channelUID);
392         if (POWER.equals(channelUID.getId())) {
393             valueReceived(POWER, OnOffType.from(getPowerState()));
394         }
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));
400         }
401     }
402
403     public void setModelName(String modelName) {
404         if (!modelName.isBlank()) {
405             this.modelName = modelName;
406         }
407     }
408
409     public String getModelName() {
410         return modelName;
411     }
412
413     public synchronized void setPowerState(boolean state) {
414         powerState = state;
415         logger.trace("{}: PowerState set to: {}", host, powerState ? "on" : "off");
416     }
417
418     public boolean getPowerState() {
419         return powerState;
420     }
421
422     public void setArtMode2022(boolean artmode) {
423         artMode2022 = artmode;
424     }
425
426     public boolean getArtMode2022() {
427         return artMode2022;
428     }
429
430     public boolean getArtModeSupported() {
431         return artModeSupported;
432     }
433
434     public synchronized void setArtModeSupported(boolean artmode) {
435         if (!artModeSupported && artmode) {
436             logger.debug("{}: ArtMode Enabled", host);
437         }
438         artModeSupported = artmode;
439     }
440
441     @Override
442     public void initialize() {
443         updateStatus(ThingStatus.UNKNOWN);
444
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");
449             return;
450         }
451
452         // note this can take up to 500ms to return if TV is off
453         discoverConfiguration();
454
455         upnpService.getRegistry().addListener(this);
456
457         checkAndCreateServices();
458     }
459
460     /**
461      * Start polling job with initial delay of 10 seconds if websocket protocol is selected
462      *
463      */
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));
471         }
472     }
473
474     private void stopPolling() {
475         pollingJob.ifPresent(job -> job.cancel(true));
476         pollingJob = Optional.empty();
477     }
478
479     @Override
480     public void dispose() {
481         logger.debug("{}: Disposing SamsungTvHandler", host);
482         stopPolling();
483         wolTask.cancel();
484         setArtMode2022(false);
485         setArtModeSupported(false);
486         artApiVersion = 0;
487         stopServices();
488         services.clear();
489         upnpService.getRegistry().removeListener(this);
490     }
491
492     private synchronized void stopServices() {
493         stopPolling();
494         if (!services.isEmpty()) {
495             if (isFrame2022()) {
496                 logger.debug("{}: Shutdown all Samsung services except RemoteControllerService", host);
497                 services.stream().forEach(a -> stopService(a));
498             } else {
499                 logger.debug("{}: Shutdown all Samsung services", host);
500                 services.stream().forEach(a -> stopService(a));
501                 services.clear();
502             }
503         }
504     }
505
506     private synchronized void shutdown() {
507         stopServices();
508         putOffline();
509     }
510
511     public synchronized void putOnline() {
512         if (getThing().getStatus() != ThingStatus.ONLINE) {
513             updateStatus(ThingStatus.ONLINE);
514             startPolling();
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
521                     fetchPowerState();
522                     valueReceived(POWER, OnOffType.from(getPowerState()));
523                 } else {
524                     valueReceived(POWER, OnOffType.ON);
525                 }
526             }
527             logger.debug("{}: TV is {}", host, getThing().getStatus());
528         }
529     }
530
531     public synchronized void putOffline() {
532         if (getThing().getStatus() != ThingStatus.OFFLINE) {
533             stopPolling();
534             valueReceived(ART_MODE, OnOffType.OFF);
535             valueReceived(POWER, OnOffType.OFF);
536             if (getArtMode2022()) {
537                 valueReceived(SET_ART_MODE, OnOffType.OFF);
538             }
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());
544         }
545     }
546
547     public boolean isChannelLinked(String ch) {
548         return isLinked(ch);
549     }
550
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));
555     }
556
557     private void poll() {
558         try {
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);
567             } else {
568                 logger.debug("{}: Polling Job exception: {}", host, e.getMessage());
569             }
570         }
571     }
572
573     public synchronized void valueReceived(String variable, State value) {
574         logger.debug("{}: Received value '{}':'{}' for thing '{}'", host, variable, value, this.getThing().getUID());
575
576         if (POWER.equals(variable)) {
577             setPowerState(OnOffType.ON.equals(value));
578         }
579         updateState(variable, value);
580     }
581
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);
585         } else {
586             logger.debug("{}: Error was reported: {}, {}", host, message, (e != null) ? e.getMessage() : "");
587         }
588         updateStatus(ThingStatus.OFFLINE, statusDetail, message);
589     }
590
591     /**
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
598      */
599     private void checkAndCreateServices() {
600         logger.debug("{}: Check and create missing services", host);
601
602         boolean isOnline = false;
603
604         // UPnP services
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;
610             }
611         }
612
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, "");
618             }
619         }
620
621         if (isOnline) {
622             putOnline();
623         } else {
624             putOffline();
625         }
626     }
627
628     /**
629      * Create or restart existing Samsung TV service.
630      * udn is used to determine whether to start upnp service or websocket
631      *
632      * @param type
633      * @param udn
634      * @param modelName
635      * @return true if service restated or created, false otherwise
636      */
637     private synchronized boolean createService(String type, String udn) {
638
639         Optional<SamsungTvService> service = findServiceInstance(type);
640
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();
645                 return true;
646             }
647             return false;
648         }
649
650         service = createNewService(type, udn);
651         if (service.isPresent()) {
652             startService(service.get());
653             logger.debug("{}: Started service for: {}, {} ({})", host, getModelName(), type, udn);
654             return true;
655         }
656         logger.trace("{}: Skipping unknown service: {}, {} ({})", host, modelName, type, udn);
657         return false;
658     }
659
660     /**
661      * Create Samsung TV service.
662      * udn is used to determine whether to start upnp service or websocket
663      *
664      * @param type
665      * @param udn
666      * @return service or null
667      */
668     private synchronized Optional<SamsungTvService> createNewService(String type, String udn) {
669         Optional<SamsungTvService> service = Optional.empty();
670
671         switch (type) {
672             case MainTVServerService.SERVICE_NAME:
673                 service = Optional.of(new MainTVServerService(upnpIOService, udn, host, this));
674                 break;
675             case MediaRendererService.SERVICE_NAME:
676                 service = Optional.of(new MediaRendererService(upnpIOService, udn, host, this));
677                 break;
678             case RemoteControllerService.SERVICE_NAME:
679                 try {
680                     if (configuration.isWebsocketProtocol() && !udn.isEmpty()) {
681                         throw new RemoteControllerException("config is websocket - ignoring UPNP service");
682                     }
683                     service = Optional
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());
687                 }
688                 break;
689             case SmartThingsApiService.SERVICE_NAME:
690                 service = Optional.of(new SmartThingsApiService(host, this));
691                 break;
692         }
693         return service;
694     }
695
696     public synchronized Optional<SamsungTvService> findServiceInstance(String serviceName) {
697         return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst();
698     }
699
700     private synchronized void startService(SamsungTvService service) {
701         service.start();
702         services.add(service);
703     }
704
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());
709             return;
710         }
711         service.stop();
712         services.remove(service);
713     }
714
715     @Override
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));
720             initializeConfig();
721             checkAndCreateServices();
722         }
723     }
724
725     @Override
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));
730                 shutdown();
731             }
732         }
733     }
734
735     @Override
736     public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
737     }
738
739     @Override
740     public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
741     }
742
743     @Override
744     public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
745             @Nullable Exception ex) {
746     }
747
748     @Override
749     public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
750     }
751
752     @Override
753     public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
754     }
755
756     @Override
757     public void beforeShutdown(@Nullable Registry registry) {
758     }
759
760     @Override
761     public void afterShutdown() {
762     }
763
764     public boolean isFrame2022() {
765         return getArtMode2022() || (getArtModeSupported() && artApiVersion >= 1);
766     }
767
768     public void setOffline() {
769         // schedule this in the future to allow calling service to return immediately
770         scheduler.submit(this::shutdown);
771     }
772
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);
781         }
782     }
783
784     public ScheduledExecutorService getScheduler() {
785         return scheduler;
786     }
787
788     public WebSocketFactory getWebSocketFactory() {
789         return webSocketFactory;
790     }
791 }