2 * Copyright (c) 2010-2021 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 for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
243 createService((RemoteDevice) device);
246 checkCreateManualConnection();
249 private synchronized void createService(RemoteDevice device) {
250 if (configuration.hostName != null
251 && configuration.hostName.equals(device.getIdentity().getDescriptorURL().getHost())) {
252 String modelName = device.getDetails().getModelDetails().getModelName();
253 String udn = device.getIdentity().getUdn().getIdentifierString();
254 String type = device.getType().getType();
256 SamsungTvService existingService = findServiceInstance(type);
258 if (existingService == null || !existingService.isUpnp()) {
259 SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
260 configuration.hostName, configuration.port);
262 if (newService != null) {
263 if (existingService != null) {
264 stopService(existingService);
265 startService(newService);
266 logger.debug("Restarting service in UPnP mode for: {}, {} ({})", modelName, type, udn);
268 startService(newService);
269 logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
272 logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
275 logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
276 existingService.clearCache();
282 private @Nullable SamsungTvService findServiceInstance(String serviceName) {
283 Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
285 for (SamsungTvService service : services) {
286 if (service.getClass() == cl) {
293 private synchronized void checkCreateManualConnection() {
295 // create remote service manually if it does not yet exist
297 RemoteControllerService service = (RemoteControllerService) findServiceInstance(
298 RemoteControllerService.SERVICE_NAME);
299 if (service == null) {
300 service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
301 startService(service);
303 // open connection again if needed
304 if (!service.checkConnection()) {
308 } catch (RuntimeException e) {
309 logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
313 private synchronized void startService(SamsungTvService service) {
314 service.addEventListener(this);
316 services.add(service);
319 private synchronized void stopService(SamsungTvService service) {
321 service.removeEventListener(this);
322 services.remove(service);
326 public void remoteDeviceAdded(@Nullable Registry registry, @Nullable RemoteDevice device) {
327 if (configuration.hostName != null && device != null && device.getIdentity() != null
328 && device.getIdentity().getDescriptorURL() == null
329 && configuration.hostName.equals(device.getIdentity().getDescriptorURL().getHost())
330 && device.getType() != null) {
331 logger.debug("remoteDeviceAdded: {}, {}", device.getType().getType(),
332 device.getIdentity().getDescriptorURL());
334 /* Check if configuration should be updated */
335 if (configuration.macAddress == null || configuration.macAddress.trim().isEmpty()) {
336 String macAddress = WakeOnLanUtility.getMACAddress(configuration.hostName);
337 if (macAddress != null) {
338 putConfig(SamsungTvConfiguration.MAC_ADDRESS, macAddress);
339 logger.debug("remoteDeviceAdded, macAddress: {}", macAddress);
342 if (SamsungTvConfiguration.PROTOCOL_NONE.equals(configuration.protocol)) {
343 Map<String, Object> properties = RemoteControllerService.discover(configuration.hostName);
344 for (Map.Entry<String, Object> property : properties.entrySet()) {
345 putConfig(property.getKey(), property.getValue());
346 logger.debug("remoteDeviceAdded, {}: {}", property.getKey(), property.getValue());
349 upnpUDN = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
350 logger.debug("remoteDeviceAdded, upnpUDN={}", upnpUDN);
351 checkAndCreateServices();
356 public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
357 if (device == null) {
360 String udn = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
361 if (udn.equals(upnpUDN)) {
362 logger.debug("Device removed: udn={}", upnpUDN);
369 public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
373 public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
377 public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
378 @Nullable Exception ex) {
382 public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
386 public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
390 public void beforeShutdown(@Nullable Registry registry) {
394 public void afterShutdown() {
398 * Send multiple WOL packets spaced with 100ms intervals and resend command
400 * @param channel Channel to resend command on
401 * @param command Command to resend
403 private void sendWOLandResendCommand(String channel, Command command) {
404 if (configuration.macAddress == null || configuration.macAddress.isEmpty()) {
405 logger.warn("Cannot send WOL packet to {} MAC address unknown", configuration.hostName);
408 logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
410 // send max 10 WOL packets with 100ms intervals
411 scheduler.schedule(new Runnable() {
417 if (count < WOL_PACKET_RETRY_COUNT) {
418 WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
419 scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
422 }, 1, TimeUnit.MILLISECONDS);
424 // after RemoteService up again to ensure state is properly set
425 scheduler.schedule(new Runnable() {
431 if (count < WOL_SERVICE_CHECK_COUNT) {
432 RemoteControllerService service = (RemoteControllerService) findServiceInstance(
433 RemoteControllerService.SERVICE_NAME);
434 if (service != null) {
435 logger.info("Service found after {} attempts: resend command {} to channel {}", count,
437 service.handleCommand(channel, command);
439 scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
442 logger.info("Service NOT found after {} attempts", count);
445 }, 1000, TimeUnit.MILLISECONDS);
450 public void putConfig(@Nullable String key, @Nullable Object value) {
451 getConfig().put(key, value);
452 configuration = getConfigAs(SamsungTvConfiguration.class);
456 public Object getConfig(@Nullable String key) {
457 return getConfig().get(key);
461 public WebSocketFactory getWebSocketFactory() {
462 return webSocketFactory;