2 * Copyright (c) 2010-2023 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.samsungtv.internal.handler;
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
19 import java.util.concurrent.CopyOnWriteArraySet;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.jupnp.UpnpService;
26 import org.jupnp.model.meta.Device;
27 import org.jupnp.model.meta.LocalDevice;
28 import org.jupnp.model.meta.RemoteDevice;
29 import org.jupnp.registry.Registry;
30 import org.jupnp.registry.RegistryListener;
31 import org.openhab.binding.samsungtv.internal.WakeOnLanUtility;
32 import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
33 import org.openhab.binding.samsungtv.internal.service.RemoteControllerService;
34 import org.openhab.binding.samsungtv.internal.service.ServiceFactory;
35 import org.openhab.binding.samsungtv.internal.service.api.EventListener;
36 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
37 import org.openhab.core.io.net.http.WebSocketFactory;
38 import org.openhab.core.io.transport.upnp.UpnpIOService;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.types.Command;
47 import org.openhab.core.types.RefreshType;
48 import org.openhab.core.types.State;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link SamsungTvHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Pauli Anttila - Initial contribution
57 * @author Martin van Wingerden - Some changes for non-UPnP configured devices
58 * @author Arjan Mels - Remove RegistryListener, manually create RemoteService in all circumstances, add sending of WOL
59 * package to power on TV
62 public class SamsungTvHandler extends BaseThingHandler implements RegistryListener, EventListener {
64 private static final int WOL_PACKET_RETRY_COUNT = 10;
65 private static final int WOL_SERVICE_CHECK_COUNT = 30;
67 private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
69 private final UpnpIOService upnpIOService;
70 private final UpnpService upnpService;
71 private final WebSocketFactory webSocketFactory;
73 private SamsungTvConfiguration configuration;
75 private @Nullable String upnpUDN = null;
77 /* Samsung TV services */
78 private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
80 /* Store powerState to be able to restore upon new link */
81 private boolean powerState = false;
83 /* Store if art mode is supported to be able to skip switching power state to ON during initialization */
84 boolean artModeIsSupported = false;
86 private @Nullable ScheduledFuture<?> pollingJob;
88 public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
89 WebSocketFactory webSocketFactory) {
92 logger.debug("Create a Samsung TV Handler for thing '{}'", getThing().getUID());
94 this.upnpIOService = upnpIOService;
95 this.upnpService = upnpService;
96 this.webSocketFactory = webSocketFactory;
97 this.configuration = getConfigAs(SamsungTvConfiguration.class);
101 public void handleCommand(ChannelUID channelUID, Command command) {
102 logger.debug("Received channel: {}, command: {}", channelUID, command);
104 String channel = channelUID.getId();
106 // if power on command try WOL for good measure:
107 if ((channel.equals(POWER) || channel.equals(ART_MODE)) && OnOffType.ON.equals(command)) {
108 sendWOLandResendCommand(channel, command);
111 // Delegate command to correct service
112 for (SamsungTvService service : services) {
113 for (String s : service.getSupportedChannelNames()) {
114 if (channel.equals(s)) {
115 service.handleCommand(channel, command);
121 logger.warn("Channel '{}' not supported", channelUID);
125 public void channelLinked(ChannelUID channelUID) {
126 logger.trace("channelLinked: {}", channelUID);
128 updateState(POWER, getPowerState() ? OnOffType.ON : OnOffType.OFF);
130 for (SamsungTvService service : services) {
131 service.clearCache();
135 private synchronized void setPowerState(boolean state) {
139 private synchronized boolean getPowerState() {
144 public void initialize() {
145 updateStatus(ThingStatus.UNKNOWN);
147 logger.debug("Initializing Samsung TV handler for uid '{}'", getThing().getUID());
149 configuration = getConfigAs(SamsungTvConfiguration.class);
151 upnpService.getRegistry().addListener(this);
153 checkAndCreateServices();
155 logger.debug("Start refresh task, interval={}", configuration.refreshInterval);
156 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, configuration.refreshInterval,
157 TimeUnit.MILLISECONDS);
161 public void dispose() {
162 logger.debug("Disposing SamsungTvHandler");
164 if (pollingJob != null) {
165 if (!pollingJob.isCancelled()) {
166 pollingJob.cancel(true);
171 upnpService.getRegistry().removeListener(this);
176 private void shutdown() {
177 logger.debug("Shutdown all Samsung services");
178 for (SamsungTvService service : services) {
179 stopService(service);
184 private synchronized void putOnline() {
186 updateStatus(ThingStatus.ONLINE);
188 if (!artModeIsSupported) {
189 updateState(POWER, OnOffType.ON);
193 private synchronized void putOffline() {
194 setPowerState(false);
195 updateStatus(ThingStatus.OFFLINE);
196 updateState(ART_MODE, OnOffType.OFF);
197 updateState(POWER, OnOffType.OFF);
198 updateState(SOURCE_APP, new StringType(""));
201 private void poll() {
202 for (SamsungTvService service : services) {
203 for (String channel : service.getSupportedChannelNames()) {
204 if (isLinked(channel)) {
205 // Avoid redundant REFRESH commands when 2 channels are linked to the same UPnP action request
206 if ((channel.equals(SOURCE_ID) && isLinked(SOURCE_NAME))
207 || (channel.equals(CHANNEL_NAME) && isLinked(PROGRAM_TITLE))) {
210 service.handleCommand(channel, RefreshType.REFRESH);
217 public synchronized void valueReceived(String variable, State value) {
218 logger.debug("Received value '{}':'{}' for thing '{}'", variable, value, this.getThing().getUID());
220 if (POWER.equals(variable)) {
221 setPowerState(OnOffType.ON.equals(value));
222 } else if (ART_MODE.equals(variable)) {
223 artModeIsSupported = true;
225 updateState(variable, value);
229 public void reportError(ThingStatusDetail statusDetail, @Nullable String message, @Nullable Throwable e) {
230 logger.debug("Error was reported: {}", message, e);
231 updateStatus(ThingStatus.OFFLINE, statusDetail, message);
235 * One Samsung TV contains several UPnP devices. Samsung TV is discovered by
236 * Media Renderer UPnP device. This function tries to find another UPnP
237 * devices related to same Samsung TV and create handler for those.
239 private void checkAndCreateServices() {
240 logger.debug("Check and create missing UPnP services");
242 boolean isOnline = false;
244 for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
245 if (createService((RemoteDevice) device)) {
251 logger.debug("Device was online");
254 logger.debug("Device was NOT online");
258 checkCreateManualConnection();
261 private synchronized boolean createService(RemoteDevice device) {
262 if (configuration.hostName != null
263 && configuration.hostName.equals(device.getIdentity().getDescriptorURL().getHost())) {
264 String modelName = device.getDetails().getModelDetails().getModelName();
265 String udn = device.getIdentity().getUdn().getIdentifierString();
266 String type = device.getType().getType();
268 SamsungTvService existingService = findServiceInstance(type);
270 if (existingService == null || !existingService.isUpnp()) {
271 SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
272 configuration.hostName, configuration.port);
274 if (newService != null) {
275 if (existingService != null) {
276 stopService(existingService);
277 startService(newService);
278 logger.debug("Restarting service in UPnP mode for: {}, {} ({})", modelName, type, udn);
280 startService(newService);
281 logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
284 logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
287 logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
288 existingService.clearCache();
295 private @Nullable SamsungTvService findServiceInstance(String serviceName) {
296 Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
298 for (SamsungTvService service : services) {
299 if (service.getClass() == cl) {
306 private synchronized void checkCreateManualConnection() {
308 // create remote service manually if it does not yet exist
310 RemoteControllerService service = (RemoteControllerService) findServiceInstance(
311 RemoteControllerService.SERVICE_NAME);
312 if (service == null) {
313 service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
314 startService(service);
316 // open connection again if needed
317 if (!service.checkConnection()) {
321 } catch (RuntimeException e) {
322 logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
326 private synchronized void startService(SamsungTvService service) {
327 service.addEventListener(this);
329 services.add(service);
332 private synchronized void stopService(SamsungTvService service) {
334 service.removeEventListener(this);
335 services.remove(service);
339 public void remoteDeviceAdded(@Nullable Registry registry, @Nullable RemoteDevice device) {
340 if (configuration.hostName != null && device != null && device.getIdentity() != null
341 && device.getIdentity().getDescriptorURL() != null
342 && configuration.hostName.equals(device.getIdentity().getDescriptorURL().getHost())
343 && device.getType() != null) {
344 logger.debug("remoteDeviceAdded: {}, {}", device.getType().getType(),
345 device.getIdentity().getDescriptorURL());
347 /* Check if configuration should be updated */
348 if (configuration.macAddress == null || configuration.macAddress.trim().isEmpty()) {
349 String macAddress = WakeOnLanUtility.getMACAddress(configuration.hostName);
350 if (macAddress != null) {
351 putConfig(SamsungTvConfiguration.MAC_ADDRESS, macAddress);
352 logger.debug("remoteDeviceAdded, macAddress: {}", macAddress);
355 if (SamsungTvConfiguration.PROTOCOL_NONE.equals(configuration.protocol)) {
356 Map<String, Object> properties = RemoteControllerService.discover(configuration.hostName);
357 for (Map.Entry<String, Object> property : properties.entrySet()) {
358 putConfig(property.getKey(), property.getValue());
359 logger.debug("remoteDeviceAdded, {}: {}", property.getKey(), property.getValue());
362 upnpUDN = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
363 logger.debug("remoteDeviceAdded, upnpUDN={}", upnpUDN);
364 checkAndCreateServices();
369 public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
370 if (device == null) {
373 String udn = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
374 if (udn.equals(upnpUDN)) {
375 logger.debug("Device removed: udn={}", upnpUDN);
378 checkCreateManualConnection();
383 public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
387 public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
391 public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
392 @Nullable Exception ex) {
396 public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
400 public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
404 public void beforeShutdown(@Nullable Registry registry) {
408 public void afterShutdown() {
412 * Send multiple WOL packets spaced with 100ms intervals and resend command
414 * @param channel Channel to resend command on
415 * @param command Command to resend
417 private void sendWOLandResendCommand(String channel, Command command) {
418 if (configuration.macAddress == null || configuration.macAddress.isEmpty()) {
419 logger.warn("Cannot send WOL packet to {} MAC address unknown", configuration.hostName);
422 logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
424 // send max 10 WOL packets with 100ms intervals
425 scheduler.schedule(new Runnable() {
431 if (count < WOL_PACKET_RETRY_COUNT) {
432 WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
433 scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
436 }, 1, TimeUnit.MILLISECONDS);
438 // after RemoteService up again to ensure state is properly set
439 scheduler.schedule(new Runnable() {
445 if (count < WOL_SERVICE_CHECK_COUNT) {
446 RemoteControllerService service = (RemoteControllerService) findServiceInstance(
447 RemoteControllerService.SERVICE_NAME);
448 if (service != null) {
449 logger.info("Service found after {} attempts: resend command {} to channel {}", count,
451 service.handleCommand(channel, command);
453 scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
456 logger.info("Service NOT found after {} attempts", count);
459 }, 1000, TimeUnit.MILLISECONDS);
464 public void putConfig(@Nullable String key, @Nullable Object value) {
465 getConfig().put(key, value);
466 configuration = getConfigAs(SamsungTvConfiguration.class);
470 public Object getConfig(@Nullable String key) {
471 return getConfig().get(key);
475 public WebSocketFactory getWebSocketFactory() {
476 return webSocketFactory;