]> git.basschouten.com Git - openhab-addons.git/blob
0fa24968072e3109e84990186aef50ffa3f365ad
[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             // 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);
572             } else {
573                 logger.debug("{}: Polling Job exception: {}", host, e.getMessage());
574             }
575         }
576     }
577
578     public synchronized void valueReceived(String variable, State value) {
579         logger.debug("{}: Received value '{}':'{}' for thing '{}'", host, variable, value, this.getThing().getUID());
580
581         if (POWER.equals(variable)) {
582             setPowerState(OnOffType.ON.equals(value));
583         }
584         updateState(variable, value);
585     }
586
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);
590         } else {
591             logger.debug("{}: Error was reported: {}, {}", host, message, (e != null) ? e.getMessage() : "");
592         }
593         updateStatus(ThingStatus.OFFLINE, statusDetail, message);
594     }
595
596     /**
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
603      */
604     private void checkAndCreateServices() {
605         logger.debug("{}: Check and create missing services", host);
606
607         boolean isOnline = false;
608
609         // UPnP services
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;
615             }
616         }
617
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, "");
623             }
624         }
625
626         if (isOnline) {
627             putOnline();
628         } else {
629             putOffline();
630         }
631     }
632
633     /**
634      * Create or restart existing Samsung TV service.
635      * udn is used to determine whether to start upnp service or websocket
636      *
637      * @param type
638      * @param udn
639      * @param modelName
640      * @return true if service restated or created, false otherwise
641      */
642     private synchronized boolean createService(String type, String udn) {
643         Optional<SamsungTvService> service = findServiceInstance(type);
644
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();
649                 return true;
650             }
651             return false;
652         }
653
654         service = createNewService(type, udn);
655         if (service.isPresent()) {
656             startService(service.get());
657             logger.debug("{}: Started service for: {}, {} ({})", host, getModelName(), type, udn);
658             return true;
659         }
660         logger.trace("{}: Skipping unknown service: {}, {} ({})", host, modelName, type, udn);
661         return false;
662     }
663
664     /**
665      * Create Samsung TV service.
666      * udn is used to determine whether to start upnp service or websocket
667      *
668      * @param type
669      * @param udn
670      * @return service or null
671      */
672     private synchronized Optional<SamsungTvService> createNewService(String type, String udn) {
673         Optional<SamsungTvService> service = Optional.empty();
674
675         switch (type) {
676             case MainTVServerService.SERVICE_NAME:
677                 service = Optional.of(new MainTVServerService(upnpIOService, udn, host, this));
678                 break;
679             case MediaRendererService.SERVICE_NAME:
680                 service = Optional.of(new MediaRendererService(upnpIOService, udn, host, this));
681                 break;
682             case RemoteControllerService.SERVICE_NAME:
683                 try {
684                     if (configuration.isWebsocketProtocol() && !udn.isEmpty()) {
685                         throw new RemoteControllerException("config is websocket - ignoring UPNP service");
686                     }
687                     service = Optional
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());
691                 }
692                 break;
693             case SmartThingsApiService.SERVICE_NAME:
694                 service = Optional.of(new SmartThingsApiService(host, this));
695                 break;
696         }
697         return service;
698     }
699
700     public synchronized Optional<SamsungTvService> findServiceInstance(String serviceName) {
701         return services.stream().filter(a -> a.getServiceName().equals(serviceName)).findFirst();
702     }
703
704     private synchronized void startService(SamsungTvService service) {
705         service.start();
706         services.add(service);
707     }
708
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());
713             return;
714         }
715         service.stop();
716         services.remove(service);
717     }
718
719     @Override
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));
724             initializeConfig();
725             checkAndCreateServices();
726         }
727     }
728
729     @Override
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));
734                 shutdown();
735             }
736         }
737     }
738
739     @Override
740     public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
741     }
742
743     @Override
744     public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
745     }
746
747     @Override
748     public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
749             @Nullable Exception ex) {
750     }
751
752     @Override
753     public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
754     }
755
756     @Override
757     public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
758     }
759
760     @Override
761     public void beforeShutdown(@Nullable Registry registry) {
762     }
763
764     @Override
765     public void afterShutdown() {
766     }
767
768     public boolean isFrame2022() {
769         return getArtMode2022() || (getArtModeSupported() && artApiVersion >= 1);
770     }
771
772     public void setOffline() {
773         // schedule this in the future to allow calling service to return immediately
774         scheduler.submit(this::shutdown);
775     }
776
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);
785         }
786     }
787
788     public ScheduledExecutorService getScheduler() {
789         return scheduler;
790     }
791
792     public WebSocketFactory getWebSocketFactory() {
793         return webSocketFactory;
794     }
795 }