]> git.basschouten.com Git - openhab-addons.git/blob
eb66d57705a1b32193aed0a13136588e421223fc
[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 static final 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 (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();
207             }
208             return properties;
209         }
210     }
211
212     /**
213      * For Modern TVs get configuration, with time delay, and retry
214      *
215      * @param ms int delay in milliseconds
216      * @param retryCount int number of retries before giving up
217      * @return TVProperties
218      */
219     public TVProperties fetchTVProperties(int ms, int retryCount) {
220         ScheduledFuture<Optional<TVProperties>> future = scheduler.schedule(new FetchTVProperties(), ms,
221                 TimeUnit.MILLISECONDS);
222         try {
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);
230                 }
231             }
232         } catch (InterruptedException | ExecutionException e) {
233             logger.warn("{}: Cannot get TVProperties: {}", host, e.getMessage());
234         }
235         logger.debug("{}: Cannot get TVProperties, return Empty properties", host);
236         return new TVProperties();
237     }
238
239     /**
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
244      *
245      */
246     private void discoverConfiguration() {
247         /* Check if configuration should be updated */
248         configuration = getConfigAs(SamsungTvConfiguration.class);
249         host = configuration.getHostName();
250         switch (configuration.getProtocol()) {
251             case PROTOCOL_NONE:
252                 if (configuration.getMacAddress().isBlank()) {
253                     String macAddress = WakeOnLanUtility.getMACAddress(host);
254                     if (macAddress != null) {
255                         putConfig(MAC_ADDRESS, macAddress);
256                     }
257                 }
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);
263                     } else {
264                         putConfig(PROTOCOL, PROTOCOL_WEBSOCKET);
265                         putConfig(PORT, PORT_DEFAULT_WEBSOCKET);
266                     }
267                     if ((configuration.getMacAddress().isBlank()) && !properties.getWifiMac().isBlank()) {
268                         putConfig(MAC_ADDRESS, properties.getWifiMac());
269                     }
270                     updateSettings(properties);
271                     break;
272                 }
273
274                 initialized = true;
275                 for (int port : PORTS) {
276                     try {
277                         RemoteControllerLegacy remoteController = new RemoteControllerLegacy(host, port, "openHAB",
278                                 "openHAB");
279                         remoteController.openConnection();
280                         remoteController.close();
281                         putConfig(PROTOCOL, SamsungTvConfiguration.PROTOCOL_LEGACY);
282                         putConfig(PORT, port);
283                         setPowerState(true);
284                         break;
285                     } catch (RemoteControllerException e) {
286                         // ignore error
287                     }
288                 }
289                 break;
290             case PROTOCOL_WEBSOCKET:
291             case PROTOCOL_SECUREWEBSOCKET:
292                 initializeConfig();
293                 if (!initialized) {
294                     logger.warn("{}: TV binding is not yet Initialized", host);
295                 }
296                 break;
297             case PROTOCOL_LEGACY:
298                 initialized = true;
299                 break;
300         }
301         showConfiguration();
302     }
303
304     public void initializeConfig() {
305         if (!initialized) {
306             TVProperties properties = fetchTVProperties(0, 0);
307             if ("on".equals(properties.getPowerState())) {
308                 updateSettings(properties);
309             }
310         }
311     }
312
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);
320             artApiVersion = 1;
321         }
322         setArtModeSupported(properties.getFrameTVSupport() && year < 22);
323         logger.debug("{}: Updated artModeSupported: {} PowerState: {}({}) artMode2022: {}", host, getArtModeSupported(),
324                 getPowerState(), properties.getPowerState(), getArtMode2022());
325         initialized = true;
326     }
327
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);
335             } else {
336                 logger.debug("{}: SmartThings enabled, device id: {}", host, configuration.getSmartThingsDeviceId());
337             }
338         }
339     }
340
341     /**
342      * get PowerState from TVProperties
343      * Note: Series 7 TV's do not have the PowerState value
344      *
345      * @return String giving power state (TV can be on or standby, off if unreachable)
346      */
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);
353         return powerState;
354     }
355
356     public boolean handleCommand(String channel, Command command, int ms) {
357         scheduler.schedule(() -> {
358             handleCommand(channel, command);
359         }, ms, TimeUnit.MILLISECONDS);
360         return true;
361     }
362
363     @Override
364     public void handleCommand(ChannelUID channelUID, Command command) {
365         logger.debug("{}: Received channel: {}, command: {}", host, channelUID, Utils.truncCmd(command));
366         handleCommand(channelUID.getId(), command);
367     }
368
369     public void handleCommand(String channel, Command command) {
370         logger.trace("{}: Received: {}, command: {}", host, channel, Utils.truncCmd(command));
371
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)) {
377                         return;
378                     }
379                 }
380             }
381         }
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());
386             } else {
387                 logger.warn("{}: Channel '{}' not connected/supported", host, channel);
388             }
389         }
390     }
391
392     @Override
393     public void channelLinked(ChannelUID channelUID) {
394         logger.trace("{}: channelLinked: {}", host, channelUID);
395         if (POWER.equals(channelUID.getId())) {
396             valueReceived(POWER, OnOffType.from(getPowerState()));
397         }
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));
403         }
404     }
405
406     public void setModelName(String modelName) {
407         if (!modelName.isBlank()) {
408             this.modelName = modelName;
409         }
410     }
411
412     public String getModelName() {
413         return modelName;
414     }
415
416     public synchronized void setPowerState(boolean state) {
417         powerState = state;
418         logger.trace("{}: PowerState set to: {}", host, powerState ? "on" : "off");
419     }
420
421     public boolean getPowerState() {
422         return powerState;
423     }
424
425     public void setArtMode2022(boolean artmode) {
426         artMode2022 = artmode;
427     }
428
429     public boolean getArtMode2022() {
430         return artMode2022;
431     }
432
433     public boolean getArtModeSupported() {
434         return artModeSupported;
435     }
436
437     public synchronized void setArtModeSupported(boolean artmode) {
438         if (!artModeSupported && artmode) {
439             logger.debug("{}: ArtMode Enabled", host);
440         }
441         artModeSupported = artmode;
442     }
443
444     @Override
445     public void initialize() {
446         updateStatus(ThingStatus.UNKNOWN);
447
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");
452             return;
453         }
454
455         // note this can take up to 500ms to return if TV is off
456         discoverConfiguration();
457
458         upnpService.getRegistry().addListener(this);
459
460         checkAndCreateServices();
461     }
462
463     /**
464      * Start polling job with initial delay of 10 seconds if websocket protocol is selected
465      *
466      */
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));
474         }
475     }
476
477     private void stopPolling() {
478         pollingJob.ifPresent(job -> job.cancel(true));
479         pollingJob = Optional.empty();
480     }
481
482     @Override
483     public void dispose() {
484         logger.debug("{}: Disposing SamsungTvHandler", host);
485         stopPolling();
486         wolTask.cancel();
487         setArtMode2022(false);
488         setArtModeSupported(false);
489         artApiVersion = 0;
490         stopServices();
491         services.clear();
492         upnpService.getRegistry().removeListener(this);
493     }
494
495     private synchronized void stopServices() {
496         stopPolling();
497         if (!services.isEmpty()) {
498             if (isFrame2022()) {
499                 logger.debug("{}: Shutdown all Samsung services except RemoteControllerService", host);
500                 services.stream().forEach(a -> stopService(a));
501             } else {
502                 logger.debug("{}: Shutdown all Samsung services", host);
503                 services.stream().forEach(a -> stopService(a));
504                 services.clear();
505             }
506         }
507     }
508
509     private synchronized void shutdown() {
510         stopServices();
511         putOffline();
512     }
513
514     public synchronized void putOnline() {
515         if (getThing().getStatus() != ThingStatus.ONLINE) {
516             updateStatus(ThingStatus.ONLINE);
517             startPolling();
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
524                     fetchPowerState();
525                     valueReceived(POWER, OnOffType.from(getPowerState()));
526                 } else {
527                     valueReceived(POWER, OnOffType.ON);
528                 }
529             }
530             logger.debug("{}: TV is {}", host, getThing().getStatus());
531         }
532     }
533
534     public synchronized void putOffline() {
535         if (getThing().getStatus() != ThingStatus.OFFLINE) {
536             stopPolling();
537             valueReceived(ART_MODE, OnOffType.OFF);
538             valueReceived(POWER, OnOffType.OFF);
539             if (getArtMode2022()) {
540                 valueReceived(SET_ART_MODE, OnOffType.OFF);
541             }
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());
547         }
548     }
549
550     public boolean isChannelLinked(String ch) {
551         return isLinked(ch);
552     }
553
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));
558     }
559
560     private void poll() {
561         try {
562             // Skip channels if service is not connected/started
563             services.stream().filter(service -> service.checkConnection())
564                     .forEach(service -> service.getSupportedChannelNames(true).stream()
565                             .filter(channel -> isLinked(channel) && !isDuplicateChannel(channel))
566                             .forEach(channel -> service.handleCommand(channel, RefreshType.REFRESH)));
567         } catch (Exception e) {
568             if (logger.isTraceEnabled()) {
569                 logger.trace("{}: Polling Job exception: ", host, e);
570             } else {
571                 logger.debug("{}: Polling Job exception: {}", host, e.getMessage());
572             }
573         }
574     }
575
576     public synchronized void valueReceived(String variable, State value) {
577         logger.debug("{}: Received value '{}':'{}' for thing '{}'", host, variable, value, this.getThing().getUID());
578
579         if (POWER.equals(variable)) {
580             setPowerState(OnOffType.ON.equals(value));
581         }
582         updateState(variable, value);
583     }
584
585     public void reportError(ThingStatusDetail statusDetail, @Nullable String message, @Nullable Throwable e) {
586         if (logger.isTraceEnabled()) {
587             logger.trace("{}: Error was reported: {}", host, message, e);
588         } else {
589             logger.debug("{}: Error was reported: {}, {}", host, message, (e != null) ? e.getMessage() : "");
590         }
591         updateStatus(ThingStatus.OFFLINE, statusDetail, message);
592     }
593
594     /**
595      * One Samsung TV contains several UPnP devices. Samsung TV is discovered by
596      * Media Renderer UPnP device. This function tries to find another UPnP
597      * devices related to same Samsung TV and create handler for those.
598      * Also attempts to create websocket services if protocol is set to websocket
599      * And at least one UPNP service is discovered
600      * Smartthings service is also started if PAT (Api key) is entered
601      */
602     private void checkAndCreateServices() {
603         logger.debug("{}: Check and create missing services", host);
604
605         boolean isOnline = false;
606
607         // UPnP services
608         for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
609             RemoteDevice rdevice = (RemoteDevice) device;
610             if (host.equals(Utils.getHost(rdevice))) {
611                 setModelName(Utils.getModelName(rdevice));
612                 isOnline = createService(Utils.getType(rdevice), Utils.getUdn(rdevice)) || isOnline;
613             }
614         }
615
616         // Websocket services and Smartthings service
617         if ((isOnline | getArtMode2022()) && configuration.isWebsocketProtocol()) {
618             createService(RemoteControllerService.SERVICE_NAME, "");
619             if (!configuration.getSmartThingsApiKey().isBlank()) {
620                 createService(SmartThingsApiService.SERVICE_NAME, "");
621             }
622         }
623
624         if (isOnline) {
625             putOnline();
626         } else {
627             putOffline();
628         }
629     }
630
631     /**
632      * Create or restart existing Samsung TV service.
633      * udn is used to determine whether to start upnp service or websocket
634      *
635      * @param type
636      * @param udn
637      * @param modelName
638      * @return true if service restated or created, false otherwise
639      */
640     private synchronized boolean createService(String type, String udn) {
641         Optional<SamsungTvService> service = findServiceInstance(type);
642
643         if (service.isPresent()) {
644             if ((!udn.isBlank() && service.get().isUpnp()) || (udn.isBlank() && !service.get().isUpnp())) {
645                 logger.debug("{}: Service rediscovered, clearing caches: {}, {} ({})", host, getModelName(), type, udn);
646                 service.get().clearCache();
647                 return true;
648             }
649             return false;
650         }
651
652         service = createNewService(type, udn);
653         if (service.isPresent()) {
654             startService(service.get());
655             logger.debug("{}: Started service for: {}, {} ({})", host, getModelName(), type, udn);
656             return true;
657         }
658         logger.trace("{}: Skipping unknown service: {}, {} ({})", host, modelName, type, udn);
659         return false;
660     }
661
662     /**
663      * Create Samsung TV service.
664      * udn is used to determine whether to start upnp service or websocket
665      *
666      * @param type
667      * @param udn
668      * @return service or null
669      */
670     private synchronized Optional<SamsungTvService> createNewService(String type, String udn) {
671         Optional<SamsungTvService> service = Optional.empty();
672
673         switch (type) {
674             case MainTVServerService.SERVICE_NAME:
675                 service = Optional.of(new MainTVServerService(upnpIOService, udn, host, this));
676                 break;
677             case MediaRendererService.SERVICE_NAME:
678                 service = Optional.of(new MediaRendererService(upnpIOService, udn, host, this));
679                 break;
680             case RemoteControllerService.SERVICE_NAME:
681                 try {
682                     if (configuration.isWebsocketProtocol() && !udn.isEmpty()) {
683                         throw new RemoteControllerException("config is websocket - ignoring UPNP service");
684                     }
685                     service = Optional
686                             .of(new RemoteControllerService(host, configuration.getPort(), !udn.isEmpty(), this));
687                 } catch (RemoteControllerException e) {
688                     logger.warn("{}: Not creating remote controller service: {}", host, e.getMessage());
689                 }
690                 break;
691             case SmartThingsApiService.SERVICE_NAME:
692                 service = Optional.of(new SmartThingsApiService(host, this));
693                 break;
694         }
695         return service;
696     }
697
698     public synchronized Optional<SamsungTvService> findServiceInstance(String serviceName) {
699         return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst();
700     }
701
702     private synchronized void startService(SamsungTvService service) {
703         service.start();
704         services.add(service);
705     }
706
707     private synchronized void stopService(SamsungTvService service) {
708         if (isFrame2022() && service.getServiceName().equals(RemoteControllerService.SERVICE_NAME)) {
709             // don't stop the remoteController service on 2022 frame TV's
710             logger.debug("{}: not stopping: {}", host, service.getServiceName());
711             return;
712         }
713         service.stop();
714         services.remove(service);
715     }
716
717     @Override
718     public void remoteDeviceAdded(@Nullable Registry registry, @Nullable RemoteDevice device) {
719         if (device != null && host.equals(Utils.getHost(device))) {
720             logger.debug("{}: remoteDeviceAdded: {}, {}, upnpUDN={}", host, Utils.getType(device),
721                     device.getIdentity().getDescriptorURL(), Utils.getUdn(device));
722             initializeConfig();
723             checkAndCreateServices();
724         }
725     }
726
727     @Override
728     public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
729         if (device != null && host.equals(Utils.getHost(device))) {
730             if (services.stream().anyMatch(s -> s.getServiceName().equals(Utils.getType(device)))) {
731                 logger.debug("{}: Device removed: {}, udn={}", host, Utils.getType(device), Utils.getUdn(device));
732                 shutdown();
733             }
734         }
735     }
736
737     @Override
738     public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
739     }
740
741     @Override
742     public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
743     }
744
745     @Override
746     public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
747             @Nullable Exception ex) {
748     }
749
750     @Override
751     public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
752     }
753
754     @Override
755     public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
756     }
757
758     @Override
759     public void beforeShutdown(@Nullable Registry registry) {
760     }
761
762     @Override
763     public void afterShutdown() {
764     }
765
766     public boolean isFrame2022() {
767         return getArtMode2022() || (getArtModeSupported() && artApiVersion >= 1);
768     }
769
770     public void setOffline() {
771         // schedule this in the future to allow calling service to return immediately
772         scheduler.submit(this::shutdown);
773     }
774
775     public void putConfig(@Nullable String key, @Nullable Object value) {
776         if (key != null && value != null) {
777             getConfig().put(key, value);
778             Configuration config = editConfiguration();
779             config.put(key, value);
780             updateConfiguration(config);
781             logger.debug("{}: Updated Configuration {}:{}", host, key, value);
782             configuration = getConfigAs(SamsungTvConfiguration.class);
783         }
784     }
785
786     public ScheduledExecutorService getScheduler() {
787         return scheduler;
788     }
789
790     public WebSocketFactory getWebSocketFactory() {
791         return webSocketFactory;
792     }
793 }