2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.androidtv.internal.protocol.philipstv;
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.philipstv.PhilipsTVBindingConstants.*;
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;
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;
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;
80 import com.fasterxml.jackson.core.JsonProcessingException;
81 import com.fasterxml.jackson.core.type.TypeReference;
82 import com.fasterxml.jackson.databind.ObjectMapper;
85 * The {@link PhilipsTVHandler} is responsible for handling commands, which are sent to one of the
88 * @author Benjamin Meyer - Initial contribution
89 * @author Ben Rosenblum - Merged into AndroidTV
92 public class PhilipsTVConnectionManager implements DiscoveryListener {
94 private final Logger logger = LoggerFactory.getLogger(getClass());
96 private AndroidTVHandler handler;
98 public PhilipsTVConfiguration config;
100 private ScheduledExecutorService scheduler;
102 private final AndroidTVTranslationProvider translationProvider;
104 private DiscoveryServiceRegistry discoveryServiceRegistry;
106 private AndroidTVDynamicStateDescriptionProvider stateDescriptionProvider;
108 private @Nullable ThingUID upnpThingUID;
110 private @Nullable ScheduledFuture<?> refreshScheduler;
112 private final Predicate<ScheduledFuture<?>> isRefreshSchedulerRunning = r -> (r != null) && !r.isCancelled();
114 private final ReentrantLock lock = new ReentrantLock();
116 private boolean isLoggedIn = false;
118 private String statusMessage = "";
120 private HttpHost target;
122 private String username = "";
123 private String password = "";
124 private String macAddress = "";
126 private @Nullable ScheduledFuture<?> deviceHealthJob;
127 private boolean isOnline = true;
128 private boolean pendingPowerOn = false;
130 public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
132 /* Philips TV services */
133 private Map<String, PhilipsTVService> channelServices = new HashMap<>();
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);
147 private void setStatus(boolean isLoggedIn) {
149 setStatus(isLoggedIn, "online.online");
151 setStatus(isLoggedIn, "offline.unknown");
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();
165 public String getStatusMessage() {
166 return statusMessage;
169 public void setLoggedIn(boolean isLoggedIn) {
170 if (this.isLoggedIn != isLoggedIn) {
171 setStatus(isLoggedIn);
175 public boolean getLoggedIn() {
179 public void updateStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail, String thingStatusMessage) {
180 if (thingStatus == ThingStatus.ONLINE) {
183 logger.trace("Updating status to {} {} {}", thingStatus, thingStatusDetail, thingStatusMessage);
184 setStatus(false, thingStatusMessage);
188 public String getMacAddress() {
189 return this.macAddress;
192 public void saveConfigs() {
193 String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
194 File folder = new File(folderName);
196 if (!folder.exists()) {
197 logger.debug("Creating directory {}", folderName);
201 String fileName = folderName + "/philipstv." + handler.getThing().getUID().getId() + ".config";
203 Map<String, String> configMap = new HashMap<>();
204 configMap.put("username", username);
205 configMap.put("password", password);
206 configMap.put("macAddress", macAddress);
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());
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()) {
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>>() {
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());
242 public void setCreds(String username, String password) {
243 this.username = username;
244 this.password = password;
248 private boolean servicePing() {
251 SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.philipstvPort);
252 try (Socket socket = new Socket()) {
253 socket.connect(socketAddress, timeout);
255 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
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
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;
270 logger.debug("{} - Device is back online. Attempting reconnection.", handler.getThingID());
273 logger.debug("{} - Device is offline.", handler.getThingID());
274 postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
275 "offline.communication-error-will-try-to-reconnect");
280 public void checkPendingPowerOn() {
281 if (pendingPowerOn) {
283 PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
284 if (powerService != null) {
285 powerService.handleCommand(CHANNEL_POWER, OnOffType.ON);
287 pendingPowerOn = false;
288 startDeviceHealthJob(5, TimeUnit.SECONDS);
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;
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")) {
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");
308 boolean hasFailed = initCredentialsRetrieval(target, command.toString());
310 postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
311 "offline.error-occured-during-retrieval-of-credentials");
315 username = this.username;
316 password = this.password;
318 if ((username.isEmpty()) || (password.isEmpty())) {
319 postUpdateThing(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
320 "offline.pairing-was-unsuccessful");
329 if ((username.isEmpty()) || (password.isEmpty())) {
330 return; // pairing process is not finished
333 boolean isLoggedIn = this.isLoggedIn;
334 Map<String, PhilipsTVService> channelServices = this.channelServices;
336 if ((!isLoggedIn) && (!channelUID.getId().equals(CHANNEL_POWER)
337 & !channelUID.getId().equals(CHANNEL_AMBILIGHT_LOUNGE_POWER))) {
338 // Check if tv turned on meanwhile
340 PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
341 if (powerService != null) {
342 powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
344 isLoggedIn = this.isLoggedIn;
348 "Cannot execute command {} for channel {}: PowerState of TV was checked and resolved to offline.",
349 command, channelUID.getId());
354 String channel = channelUID.getId();
355 long startTime = System.currentTimeMillis();
356 // Delegate the other commands to correct channel service
358 PhilipsTVService philipsTvService = channelServices.get(channel);
360 if (philipsTvService == null) {
361 logger.warn("Unknown channel for Philips TV Binding: {}", channel);
365 if ((!isLoggedIn) && (channelUID.getId().equals(CHANNEL_POWER)) && (command.equals(OnOffType.ON))) {
366 startDeviceHealthJob(1, TimeUnit.SECONDS);
367 pendingPowerOn = true;
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);
376 public void initialize() {
377 logger.debug("Init of handler for Thing: {}", handler.getThingID());
380 String username = this.username;
381 String password = this.password;
383 if ((username.isEmpty()) || (password.isEmpty())) {
384 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
385 "offline.pairing-is-not-configured-yet");
390 startDeviceHealthJob(5, TimeUnit.SECONDS);
393 private void startDeviceHealthJob(int interval, TimeUnit unit) {
394 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
395 if (deviceHealthJob != null) {
396 deviceHealthJob.cancel(true);
398 this.deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, interval, interval, unit);
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);
408 if (!config.useUpnpDiscovery && isSchedulerInitializable()) {
409 logger.debug("connect starting refresh scheduler");
410 startRefreshScheduler();
413 CloseableHttpClient httpClient;
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()));
423 ConnectionManager connectionManager = new ConnectionManager(httpClient, target);
425 if (macAddress.isEmpty()) {
427 Optional<String> wolAddress = WakeOnLanUtil.getMacFromEnabledInterface(connectionManager);
428 if (wolAddress.isPresent()) {
429 this.macAddress = wolAddress.get();
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.");
435 } catch (IOException e) {
436 logger.debug("Error occurred during retrieval of MAC Address: {}", e.getMessage());
440 startServices(connectionManager);
442 discoveryServiceRegistry.addDiscoveryListener(this);
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");
447 Map<String, PhilipsTVService> channelServices = this.channelServices;
449 PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
450 if (powerService != null) {
451 powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
455 private void startServices(ConnectionManager connectionManager) {
456 Map<String, PhilipsTVService> services = new HashMap<>();
458 PhilipsTVService volumeService = new VolumeService(this, connectionManager);
459 services.put(CHANNEL_VOLUME, volumeService);
460 services.put(CHANNEL_MUTE, volumeService);
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);
467 PhilipsTVService keyPressService = new KeyPressService(this, connectionManager);
468 services.put(CHANNEL_KEYPRESS, keyPressService);
469 services.put(CHANNEL_PLAYER, keyPressService);
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);
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);
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);
494 * Starts the pairing Process with the TV, which results in a Pairing Code shown on TV.
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);
504 private boolean initCredentialsRetrieval(HttpHost target, String pincode) {
505 boolean hasFailed = false;
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();
510 if (pincode.isEmpty()) {
511 pairing.finishPairingWithTv(config.pairingCode, this, target);
513 pairing.finishPairingWithTv(pincode, this, target);
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);
526 // callback methods for channel services
527 public void postUpdateChannel(String channelUID, State state) {
528 handler.updateChannelState(channelUID, state);
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);
537 handler.updateChannelState(CHANNEL_POWER, OnOffType.ON);
538 startDeviceHealthJob(5, TimeUnit.SECONDS);
539 pendingPowerOn = false;
541 if (isSchedulerInitializable()) { // Init refresh scheduler only, if pairing is completed
542 startRefreshScheduler();
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();
553 // Reset app and channel list (if existing) for new retrieval during next startup
554 Map<String, PhilipsTVService> channelServices = this.channelServices;
556 PhilipsTVService appnameService = channelServices.get(CHANNEL_APPNAME);
557 if (appnameService != null) {
558 ((AppService) appnameService).clearAvailableAppList();
561 PhilipsTVService tvchannelService = channelServices.get(CHANNEL_TV_CHANNEL);
562 if (tvchannelService != null) {
563 ((TvChannelService) tvchannelService).clearAvailableTvChannelList();
567 updateStatus(status, statusDetail, msg);
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();
578 return (!username.isEmpty()) && (!password.isEmpty()) && ((refreshScheduler == null) || schedulerIsDone);
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();
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);
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);
606 private void refreshTvProperties() {
608 boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
609 if (isLockAcquired) {
612 Map<String, PhilipsTVService> channelServices = this.channelServices;
614 PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
615 if (powerService != null) {
616 powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
619 PhilipsTVService volumeService = channelServices.get(CHANNEL_VOLUME);
620 if (volumeService != null) {
621 volumeService.handleCommand(CHANNEL_VOLUME, RefreshType.REFRESH);
624 PhilipsTVService appnameService = channelServices.get(CHANNEL_APPNAME);
625 if (appnameService != null) {
626 appnameService.handleCommand(CHANNEL_APPNAME, RefreshType.REFRESH);
629 PhilipsTVService tvchannelService = channelServices.get(CHANNEL_TV_CHANNEL);
630 if (tvchannelService != null) {
631 tvchannelService.handleCommand(CHANNEL_TV_CHANNEL, RefreshType.REFRESH);
638 } catch (InterruptedException e) {
639 logger.warn("Exception occurred during refreshing the tv properties: {}", e.getMessage());
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);
653 public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
654 logger.debug("thingDiscovered: {}", result);
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;
661 PhilipsTVService powerService = channelServices.get(CHANNEL_POWER);
662 if (powerService != null) {
663 powerService.handleCommand(CHANNEL_POWER, RefreshType.REFRESH);
669 public void thingRemoved(DiscoveryService discoveryService, ThingUID thingUID) {
670 logger.debug("thingRemoved: {}", thingUID);
672 if (thingUID.equals(upnpThingUID)) {
673 postUpdateThing(ThingStatus.ONLINE, ThingStatusDetail.NONE, "online.standby");
678 public @Nullable Collection<ThingUID> removeOlderResults(DiscoveryService discoveryService, long l,
679 @Nullable Collection<ThingTypeUID> collection, @Nullable ThingUID thingUID) {
680 return Collections.emptyList();
683 public void dispose() {
684 discoveryServiceRegistry.removeDiscoveryListener(this);
685 ScheduledFuture<?> refreshScheduler = this.refreshScheduler;
686 if (refreshScheduler != null) {
687 if (isRefreshSchedulerRunning.test(refreshScheduler)) {
688 stopRefreshScheduler();
691 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
692 if (deviceHealthJob != null) {
693 deviceHealthJob.cancel(true);