]> git.basschouten.com Git - openhab-addons.git/blob
30c49311eaccee6bfd5fea213eea1782cf44410d
[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.androidtv.internal.protocol.philipstv;
14
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.net.ConnectException;
21 import java.net.InetSocketAddress;
22 import java.net.NoRouteToHostException;
23 import java.net.Socket;
24 import java.net.SocketAddress;
25 import java.net.SocketTimeoutException;
26 import java.nio.file.Files;
27 import java.nio.file.Paths;
28 import java.security.KeyManagementException;
29 import java.security.KeyStoreException;
30 import java.security.NoSuchAlgorithmException;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Optional;
38 import java.util.concurrent.ScheduledExecutorService;
39 import java.util.concurrent.ScheduledFuture;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.locks.ReentrantLock;
42 import java.util.function.Predicate;
43
44 import org.apache.http.HttpHost;
45 import org.apache.http.impl.client.CloseableHttpClient;
46 import org.eclipse.jdt.annotation.NonNullByDefault;
47 import org.eclipse.jdt.annotation.Nullable;
48 import org.openhab.binding.androidtv.internal.AndroidTVDynamicStateDescriptionProvider;
49 import org.openhab.binding.androidtv.internal.AndroidTVHandler;
50 import org.openhab.binding.androidtv.internal.AndroidTVTranslationProvider;
51 import org.openhab.binding.androidtv.internal.protocol.philipstv.pairing.PhilipsTVPairing;
52 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.AmbilightService;
53 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.AppService;
54 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.KeyPressService;
55 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.PowerService;
56 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.SearchContentService;
57 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.TvChannelService;
58 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.TvPictureService;
59 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.VolumeService;
60 import org.openhab.binding.androidtv.internal.protocol.philipstv.service.api.PhilipsTVService;
61 import org.openhab.core.OpenHAB;
62 import org.openhab.core.config.discovery.DiscoveryListener;
63 import org.openhab.core.config.discovery.DiscoveryResult;
64 import org.openhab.core.config.discovery.DiscoveryService;
65 import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.StringType;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.ThingTypeUID;
72 import org.openhab.core.thing.ThingUID;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.State;
76 import org.openhab.core.types.StateOption;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79
80 import com.fasterxml.jackson.core.JsonProcessingException;
81 import com.fasterxml.jackson.core.type.TypeReference;
82 import com.fasterxml.jackson.databind.ObjectMapper;
83
84 /**
85  * The {@link PhilipsTVHandler} is responsible for handling commands, which are sent to one of the
86  * channels.
87  *
88  * @author Benjamin Meyer - Initial contribution
89  * @author Ben Rosenblum - Merged into AndroidTV
90  */
91 @NonNullByDefault
92 public class PhilipsTVConnectionManager implements DiscoveryListener {
93
94     private final Logger logger = LoggerFactory.getLogger(getClass());
95
96     private AndroidTVHandler handler;
97
98     public PhilipsTVConfiguration config;
99
100     private ScheduledExecutorService scheduler;
101
102     private final AndroidTVTranslationProvider translationProvider;
103
104     private DiscoveryServiceRegistry discoveryServiceRegistry;
105
106     private AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider;
107
108     private @Nullable ThingUID upnpThingUID;
109
110     private @Nullable ScheduledFuture<?> refreshScheduler;
111
112     private final Predicate<ScheduledFuture<?>> isRefreshSchedulerRunning = r -> (r != null) && !r.isCancelled();
113
114     private final ReentrantLock lock = new ReentrantLock();
115
116     private boolean isLoggedIn = false;
117
118     private String statusMessage = "";
119
120     private HttpHost target;
121
122     private String username = "";
123     private String password = "";
124     private String macAddress = "";
125
126     private @Nullable ScheduledFuture<?> deviceHealthJob;
127     private boolean isOnline = true;
128     private boolean pendingPowerOn = false;
129
130     public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
131
132     /* Philips TV services */
133     private Map<String, PhilipsTVService> channelServices = new HashMap<>();
134
135     public PhilipsTVConnectionManager(AndroidTVHandler handler, PhilipsTVConfiguration config) {
136         logger.debug("Create a Philips TV Handler for thing '{}'", handler.getThingUID());
137         this.handler = handler;
138         this.config = config;
139         this.scheduler = handler.getScheduler();
140         this.translationProvider = handler.getTranslationProvider();
141         this.discoveryServiceRegistry = handler.getDiscoveryServiceRegistry();
142         this.stateDescriptionProvider = handler.getStateDescriptionProvider();
143         this.target = new HttpHost(config.ipAddress, config.philipstvPort, HTTPS);
144         initialize();
145     }
146
147     private void setStatus(boolean isLoggedIn) {
148         if (isLoggedIn) {
149             setStatus(isLoggedIn, "online.online");
150         } else {
151             setStatus(isLoggedIn, "offline.unknown");
152         }
153     }
154
155     private void setStatus(boolean isLoggedIn, String statusMessage) {
156         String translatedMessage = translationProvider.getText(statusMessage);
157         logger.trace("setStatus to {} {} {}", isLoggedIn, statusMessage, translatedMessage);
158         if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(translatedMessage))) {
159             this.isLoggedIn = isLoggedIn;
160             this.statusMessage = translatedMessage;
161             handler.checkThingStatus();
162         }
163     }
164
165     public String getStatusMessage() {
166         return statusMessage;
167     }
168
169     public void setLoggedIn(boolean isLoggedIn) {
170         if (this.isLoggedIn != isLoggedIn) {
171             setStatus(isLoggedIn);
172         }
173     }
174
175     public boolean getLoggedIn() {
176         return isLoggedIn;
177     }
178
179     public void updateStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail, String thingStatusMessage) {
180         if (thingStatus == ThingStatus.ONLINE) {
181             setLoggedIn(true);
182         } else {
183             logger.trace("Updating status to {} {} {}", thingStatus, thingStatusDetail, thingStatusMessage);
184             setStatus(false, thingStatusMessage);
185         }
186     }
187
188     public String getMacAddress() {
189         return this.macAddress;
190     }
191
192     public void saveConfigs() {
193         String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
194         File folder = new File(folderName);
195
196         if (!folder.exists()) {
197             logger.debug("Creating directory {}", folderName);
198             folder.mkdirs();
199         }
200
201         String fileName = folderName + "/philipstv." + handler.getThing().getUID().getId() + ".config";
202
203         Map<String, String> configMap = new HashMap<>();
204         configMap.put("username", username);
205         configMap.put("password", password);
206         configMap.put("macAddress", macAddress);
207
208         try {
209             String configJson = OBJECT_MAPPER.writeValueAsString(configMap);
210             logger.debug("Writing configJson \"{}\" to {}", configJson, fileName);
211             Files.write(Paths.get(fileName), configJson.getBytes());
212         } catch (JsonProcessingException e) {
213             logger.warn("JsonProcessingException trying to save configMap: {}", e.getMessage(), e);
214         } catch (IOException ex) {
215             logger.debug("IOException when writing configJson to file {}", ex.getMessage());
216         }
217     }
218
219     private void readConfigs() {
220         String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
221         String fileName = folderName + "/philipstv." + handler.getThing().getUID().getId() + ".config";
222         File file = new File(fileName);
223         if (!file.exists()) {
224             return;
225         }
226         try {
227             final byte[] contents = Files.readAllBytes(Paths.get(fileName));
228             String configJson = new String(contents);
229             logger.debug("Read configJson \"{}\" from {}", configJson, fileName);
230             Map<String, String> configMap = OBJECT_MAPPER.readValue(configJson,
231                     new TypeReference<HashMap<String, String>>() {
232                     });
233             this.username = Optional.ofNullable(configMap.get("username")).orElse("");
234             this.password = Optional.ofNullable(configMap.get("password")).orElse("");
235             this.macAddress = Optional.ofNullable(configMap.get("macAddress")).orElse("");
236             logger.debug("Processed configJson as {} {} {}", this.username, this.password, this.macAddress);
237         } catch (IOException ex) {
238             logger.debug("IOException when reading configJson from file {}", ex.getMessage());
239         }
240     }
241
242     public void setCreds(String username, String password) {
243         this.username = username;
244         this.password = password;
245         saveConfigs();
246     }
247
248     private boolean servicePing() {
249         int timeout = 500;
250
251         SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.philipstvPort);
252         try (Socket socket = new Socket()) {
253             socket.connect(socketAddress, timeout);
254             return true;
255         } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
256             return false;
257         } catch (IOException ignored) {
258             // IOException is thrown by automatic close() of the socket.
259             // This should actually never return a value as we should return true above already
260             return true;
261         }
262     }
263
264     private void checkHealth() {
265         boolean isOnline = servicePing();
266         logger.debug("{} - Device Health - Online: {} - Logged In: {}", handler.getThingID(), isOnline, isLoggedIn);
267         if (isOnline != this.isOnline) {
268             this.isOnline = isOnline;
269             if (isOnline) {
270                 logger.debug("{} - Device is back online.  Attempting reconnection.", handler.getThingID());
271                 connect();
272             } else {
273                 logger.debug("{} - Device is offline.", handler.getThingID());
274                 postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
275                         "offline.communication-error-will-try-to-reconnect");
276             }
277         }
278     }
279
280     public void checkPendingPowerOn() {
281         if (pendingPowerOn) {
282             @Nullable
283             PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
284             if (powerService != null) {
285                 powerService.handleCommand(CHANNEL_POWER, OnOffType.ON);
286             }
287             pendingPowerOn = false;
288             startDeviceHealthJob(5, TimeUnit.SECONDS);
289         }
290     }
291
292     public void handleCommand(ChannelUID channelUID, Command command) {
293         logger.debug("Received channel: {}, command: {}", channelUID, command);
294         String username = this.username;
295         String password = this.password;
296
297         if (channelUID.getId().equals(CHANNEL_PINCODE)) {
298             if (command instanceof StringType) {
299                 HttpHost target = new HttpHost(config.ipAddress, config.philipstvPort, HTTPS);
300                 if (command.toString().equals("REQUEST")) {
301                     try {
302                         initPairingCodeRetrieval(target);
303                     } catch (IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
304                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
305                                 "offline.error-occured-while-presenting-pairing-code");
306                     }
307                 } else {
308                     boolean hasFailed = initCredentialsRetrieval(target, command.toString());
309                     if (hasFailed) {
310                         postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
311                                 "offline.error-occured-during-retrieval-of-credentials");
312                         return;
313                     }
314                     readConfigs();
315                     username = this.username;
316                     password = this.password;
317
318                     if ((username.isEmpty()) || (password.isEmpty())) {
319                         postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
320                                 "offline.pairing-was-unsuccessful");
321                         return;
322                     }
323
324                 }
325             }
326             return;
327         }
328
329         if ((username.isEmpty()) || (password.isEmpty())) {
330             return; // pairing process is not finished
331         }
332
333         boolean isLoggedIn = this.isLoggedIn;
334         Map<String, PhilipsTVService> channelServices = this.channelServices;
335
336         if ((!isLoggedIn) && (!channelUID.getId().equals(CHANNEL_POWER)
337                 & !channelUID.getId().equals(CHANNEL_AMBILIGHT_LOUNGE_POWER))) {
338             // Check if tv turned on meanwhile
339             @Nullable
340             PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
341             if (powerService != null) {
342                 powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
343             }
344             isLoggedIn = this.isLoggedIn;
345             if (!isLoggedIn) {
346                 // still offline
347                 logger.warn(
348                         "Cannot execute command {} for channel {}: PowerState of TV was checked and resolved to offline.",
349                         command, channelUID.getId());
350                 return;
351             }
352         }
353
354         String channel = channelUID.getId();
355         long startTime = System.currentTimeMillis();
356         // Delegate the other commands to correct channel service
357         @Nullable
358         PhilipsTVService philipsTvService = channelServices.get(channel);
359
360         if (philipsTvService == null) {
361             logger.warn("Unknown channel for Philips TV Binding: {}", channel);
362             return;
363         }
364
365         if ((!isLoggedIn) && (channelUID.getId().equals(CHANNEL_POWER)) && (command.equals(OnOffType.ON))) {
366             startDeviceHealthJob(1, TimeUnit.SECONDS);
367             pendingPowerOn = true;
368         }
369
370         philipsTvService.handleCommand(channel, command);
371         long stopTime = System.currentTimeMillis();
372         long elapsedTime = stopTime - startTime;
373         logger.trace("The command {} took : {} nanoseconds", command.toFullString(), elapsedTime);
374     }
375
376     public void initialize() {
377         logger.debug("Init of handler for Thing: {}", handler.getThingID());
378
379         readConfigs();
380         String username = this.username;
381         String password = this.password;
382
383         if ((username.isEmpty()) || (password.isEmpty())) {
384             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
385                     "offline.pairing-is-not-configured-yet");
386             return;
387         }
388
389         connect();
390         startDeviceHealthJob(5, TimeUnit.SECONDS);
391     }
392
393     private void startDeviceHealthJob(int interval, TimeUnit unit) {
394         ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
395         if (deviceHealthJob != null) {
396             deviceHealthJob.cancel(true);
397         }
398         this.deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, interval, interval, unit);
399     }
400
401     private void connect() {
402         HttpHost target = this.target;
403         String username = this.username;
404         String password = this.password;
405         String macAddress = this.macAddress;
406         logger.debug("Starting connection to {} {} {}", username, password, macAddress);
407
408         if (!config.useUpnpDiscovery && isSchedulerInitializable()) {
409             logger.debug("connect starting refresh scheduler");
410             startRefreshScheduler();
411         }
412
413         CloseableHttpClient httpClient;
414
415         try {
416             httpClient = ConnectionManagerUtil.createSharedHttpClient(target, username, password);
417         } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
418             postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
419                     String.format("offline.error-occurred-during-creation-of-http-client: %s", e.getMessage()));
420             return;
421         }
422
423         ConnectionManager connectionManager = new ConnectionManager(httpClient, target);
424
425         if (macAddress.isEmpty()) {
426             try {
427                 Optional<String> wolAddress = WakeOnLanUtil.getMacFromEnabledInterface(connectionManager);
428                 if (wolAddress.isPresent()) {
429                     this.macAddress = wolAddress.get();
430                     saveConfigs();
431                 } else {
432                     logger.debug("MAC Address could not be determined for Wake-On-LAN support, "
433                             + "because Wake-On-LAN is not enabled on the TV.");
434                 }
435             } catch (IOException e) {
436                 logger.debug("Error occurred during retrieval of MAC Address: {}", e.getMessage());
437             }
438         }
439
440         startServices(connectionManager);
441
442         discoveryServiceRegistry.addDiscoveryListener(this);
443
444         // Thing is initialized, check power state and available communication of the TV and set ONLINE or OFFLINE
445         postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.online");
446
447         Map<String, PhilipsTVService> channelServices = this.channelServices;
448         @Nullable
449         PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
450         if (powerService != null) {
451             powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
452         }
453     }
454
455     private void startServices(ConnectionManager connectionManager) {
456         Map<String, PhilipsTVService> services = new HashMap<>();
457
458         PhilipsTVService volumeService = new VolumeService(this, connectionManager);
459         services.put(CHANNEL_VOLUME, volumeService);
460         services.put(CHANNEL_MUTE, volumeService);
461
462         PhilipsTVService tvPictureService = new TvPictureService(this, connectionManager);
463         services.put(CHANNEL_BRIGHTNESS, tvPictureService);
464         services.put(CHANNEL_SHARPNESS, tvPictureService);
465         services.put(CHANNEL_CONTRAST, tvPictureService);
466
467         PhilipsTVService keyPressService = new KeyPressService(this, connectionManager);
468         services.put(CHANNEL_KEYPRESS, keyPressService);
469         services.put(CHANNEL_PLAYER, keyPressService);
470
471         PhilipsTVService appService = new AppService(this, connectionManager);
472         services.put(CHANNEL_APP, appService);
473         services.put(CHANNEL_APPNAME, appService);
474         services.put(CHANNEL_APP_ICON, appService);
475
476         PhilipsTVService ambilightService = new AmbilightService(this, connectionManager);
477         services.put(CHANNEL_AMBILIGHT_POWER, ambilightService);
478         services.put(CHANNEL_AMBILIGHT_HUE_POWER, ambilightService);
479         services.put(CHANNEL_AMBILIGHT_LOUNGE_POWER, ambilightService);
480         services.put(CHANNEL_AMBILIGHT_STYLE, ambilightService);
481         services.put(CHANNEL_AMBILIGHT_COLOR, ambilightService);
482         services.put(CHANNEL_AMBILIGHT_LEFT_COLOR, ambilightService);
483         services.put(CHANNEL_AMBILIGHT_RIGHT_COLOR, ambilightService);
484         services.put(CHANNEL_AMBILIGHT_TOP_COLOR, ambilightService);
485         services.put(CHANNEL_AMBILIGHT_BOTTOM_COLOR, ambilightService);
486
487         services.put(CHANNEL_TV_CHANNEL, new TvChannelService(this, connectionManager));
488         services.put(CHANNEL_POWER, new PowerService(this, connectionManager));
489         services.put(CHANNEL_SEARCH_CONTENT, new SearchContentService(this, connectionManager));
490         channelServices = Collections.unmodifiableMap(services);
491     }
492
493     /**
494      * Starts the pairing Process with the TV, which results in a Pairing Code shown on TV.
495      */
496     private void initPairingCodeRetrieval(HttpHost target)
497             throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
498         logger.info("Pairing code for tv authentication is missing. "
499                 + "Starting initial pairing process. Please provide manually the pairing code shown on the tv at the configuration of the tv thing.");
500         PhilipsTVPairing pairing = new PhilipsTVPairing();
501         pairing.requestPairingPin(target);
502     }
503
504     private boolean initCredentialsRetrieval(HttpHost target, String pincode) {
505         boolean hasFailed = false;
506         logger.info(
507                 "Pairing code is available, but username and/or password is missing. Therefore we try to grant authorization and retrieve username and password.");
508         PhilipsTVPairing pairing = new PhilipsTVPairing();
509         try {
510             if (pincode.isEmpty()) {
511                 pairing.finishPairingWithTv(config.pairingCode, this, target);
512             } else {
513                 pairing.finishPairingWithTv(pincode, this, target);
514             }
515             postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING,
516                     "offline.authentication-with-philips-tv-device-was-successful-continuing-initialization-of-the-tv");
517         } catch (Exception e) {
518             postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
519                     "offline.could-not-successfully-finish-pairing-process-with-the-tv");
520             logger.warn("Error during finishing pairing process with the TV: {}", e.getMessage(), e);
521             hasFailed = true;
522         }
523         return hasFailed;
524     }
525
526     // callback methods for channel services
527     public void postUpdateChannel(String channelUID, State state) {
528         handler.updateChannelState(channelUID, state);
529     }
530
531     public synchronized void postUpdateThing(ThingStatus status, ThingStatusDetail statusDetail, String msg) {
532         logger.trace("postUpdateThing {} {} {}", status, statusDetail, msg);
533         if (status == ThingStatus.ONLINE) {
534             if (msg.equalsIgnoreCase(STANDBY_MSG)) {
535                 handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
536             } else {
537                 handler.updateChannelState(CHANNEL_POWER, OnOffType.ON);
538                 startDeviceHealthJob(5, TimeUnit.SECONDS);
539                 pendingPowerOn = false;
540             }
541             if (isSchedulerInitializable()) { // Init refresh scheduler only, if pairing is completed
542                 startRefreshScheduler();
543             }
544         } else if (status == ThingStatus.OFFLINE) {
545             handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
546             if (!TV_NOT_LISTENING_MSG.equals(msg)) { // avoid cancelling refresh if TV is temporarily not available
547                 ScheduledFuture<?> refreshScheduler = this.refreshScheduler;
548                 if (refreshScheduler != null) {
549                     if (config.useUpnpDiscovery && isRefreshSchedulerRunning.test(refreshScheduler)) {
550                         stopRefreshScheduler();
551                     }
552                 }
553                 // Reset app and channel list (if existing) for new retrieval during next startup
554                 Map<String, PhilipsTVService> channelServices = this.channelServices;
555                 @Nullable
556                 PhilipsTVService appnameService = channelServices.get(CHANNEL_APPNAME);
557                 if (appnameService != null) {
558                     ((AppService) appnameService).clearAvailableAppList();
559                 }
560                 @Nullable
561                 PhilipsTVService tvchannelService = channelServices.get(CHANNEL_TV_CHANNEL);
562                 if (tvchannelService != null) {
563                     ((TvChannelService) tvchannelService).clearAvailableTvChannelList();
564                 }
565             }
566         }
567         updateStatus(status, statusDetail, msg);
568     }
569
570     private boolean isSchedulerInitializable() {
571         String username = this.username;
572         String password = this.password;
573         boolean schedulerIsDone = false;
574         ScheduledFuture<?> refreshScheduler = this.refreshScheduler;
575         if (refreshScheduler != null) {
576             schedulerIsDone = refreshScheduler.isDone();
577         }
578         return (!username.isEmpty()) && (!password.isEmpty()) && ((refreshScheduler == null) || schedulerIsDone);
579     }
580
581     private void startRefreshScheduler() {
582         int configuredRefreshRateOrDefault = Optional.ofNullable(config.refreshRate).orElse(10);
583         if (configuredRefreshRateOrDefault > 0) { // If value equals zero, refreshing should not be scheduled
584             ScheduledFuture<?> refreshScheduler = this.refreshScheduler;
585             if (refreshScheduler != null) {
586                 logger.debug("Refresh Scheduler already started for Philips TV {}, terminating.", handler.getThingID());
587                 if (isRefreshSchedulerRunning.test(refreshScheduler)) {
588                     stopRefreshScheduler();
589                 }
590             }
591             logger.debug("Starting Refresh Scheduler for Philips TV {} with refresh rate of {}.", handler.getThingID(),
592                     configuredRefreshRateOrDefault);
593             this.refreshScheduler = scheduler.scheduleWithFixedDelay(this::refreshTvProperties, 10,
594                     configuredRefreshRateOrDefault, TimeUnit.SECONDS);
595         }
596     }
597
598     private void stopRefreshScheduler() {
599         logger.debug("Stopping Refresh Scheduler for Philips TV: {}", handler.getThingID());
600         ScheduledFuture<?> refreshScheduler = this.refreshScheduler;
601         if (refreshScheduler != null) {
602             refreshScheduler.cancel(true);
603         }
604     }
605
606     private void refreshTvProperties() {
607         try {
608             boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
609             if (isLockAcquired) {
610                 try {
611                     if (isOnline) {
612                         Map<String, PhilipsTVService> channelServices = this.channelServices;
613                         @Nullable
614                         PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
615                         if (powerService != null) {
616                             powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
617                         }
618                         @Nullable
619                         PhilipsTVService volumeService = channelServices.get(CHANNEL_VOLUME);
620                         if (volumeService != null) {
621                             volumeService.handleCommand(CHANNEL_VOLUME, RefreshType.REFRESH);
622                         }
623                         @Nullable
624                         PhilipsTVService appnameService = channelServices.get(CHANNEL_APPNAME);
625                         if (appnameService != null) {
626                             appnameService.handleCommand(CHANNEL_APPNAME, RefreshType.REFRESH);
627                         }
628                         @Nullable
629                         PhilipsTVService tvchannelService = channelServices.get(CHANNEL_TV_CHANNEL);
630                         if (tvchannelService != null) {
631                             tvchannelService.handleCommand(CHANNEL_TV_CHANNEL, RefreshType.REFRESH);
632                         }
633                     }
634                 } finally {
635                     lock.unlock();
636                 }
637             }
638         } catch (InterruptedException e) {
639             logger.warn("Exception occurred during refreshing the tv properties: {}", e.getMessage());
640         }
641     }
642
643     public void updateChannelStateDescription(final String channelId, Map<String, String> values) {
644         AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider = this.stateDescriptionProvider;
645         List<StateOption> options = new ArrayList<>();
646         if (!values.isEmpty()) {
647             values.forEach((key, value) -> options.add(new StateOption(key, value)));
648             stateDescriptionProvider.setStateOptions(new ChannelUID(handler.getThingUID(), channelId), options);
649         }
650     }
651
652     @Override
653     public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
654         logger.debug("thingDiscovered: {}", result);
655
656         if (config.useUpnpDiscovery && config.ipAddress.equals(result.getProperties().get(HOST))) {
657             upnpThingUID = result.getThingUID();
658             logger.debug("thingDiscovered, thingUID={}, discoveredUID={}", handler.getThingUID(), upnpThingUID);
659             Map<String, PhilipsTVService> channelServices = this.channelServices;
660             @Nullable
661             PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
662             if (powerService != null) {
663                 powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
664             }
665         }
666     }
667
668     @Override
669     public void thingRemoved(DiscoveryService discoveryService, ThingUID thingUID) {
670         logger.debug("thingRemoved: {}", thingUID);
671
672         if (thingUID.equals(upnpThingUID)) {
673             postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.standby");
674         }
675     }
676
677     @Override
678     public @Nullable Collection<ThingUID> removeOlderResults(DiscoveryService discoveryService, long l,
679             @Nullable Collection<ThingTypeUID> collection, @Nullable ThingUID thingUID) {
680         return Collections.emptyList();
681     }
682
683     public void dispose() {
684         discoveryServiceRegistry.removeDiscoveryListener(this);
685         ScheduledFuture<?> refreshScheduler = this.refreshScheduler;
686         if (refreshScheduler != null) {
687             if (isRefreshSchedulerRunning.test(refreshScheduler)) {
688                 stopRefreshScheduler();
689             }
690         }
691         ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
692         if (deviceHealthJob != null) {
693             deviceHealthJob.cancel(true);
694         }
695     }
696 }