2 * Copyright (c) 2010-2020 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.*;
17 import java.util.Collection;
18 import java.util.Collections;
21 import java.util.concurrent.CopyOnWriteArraySet;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.jupnp.UpnpService;
28 import org.jupnp.model.meta.Device;
29 import org.jupnp.model.meta.RemoteDevice;
30 import org.openhab.binding.samsungtv.internal.WakeOnLanUtility;
31 import org.openhab.binding.samsungtv.internal.config.SamsungTvConfiguration;
32 import org.openhab.binding.samsungtv.internal.service.RemoteControllerService;
33 import org.openhab.binding.samsungtv.internal.service.ServiceFactory;
34 import org.openhab.binding.samsungtv.internal.service.api.EventListener;
35 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
36 import org.openhab.core.config.discovery.DiscoveryListener;
37 import org.openhab.core.config.discovery.DiscoveryResult;
38 import org.openhab.core.config.discovery.DiscoveryService;
39 import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
40 import org.openhab.core.io.net.http.WebSocketFactory;
41 import org.openhab.core.io.transport.upnp.UpnpIOService;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.ThingUID;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link SamsungTvHandler} is responsible for handling commands, which are
59 * sent to one of the channels.
61 * @author Pauli Anttila - Initial contribution
62 * @author Martin van Wingerden - Some changes for non-UPnP configured devices
63 * @author Arjan Mels - Remove RegistryListener, manually create RemoteService in all circumstances, add sending of WOL
64 * package to power on TV
67 public class SamsungTvHandler extends BaseThingHandler implements DiscoveryListener, EventListener {
69 private static final int WOL_PACKET_RETRY_COUNT = 10;
70 private static final int WOL_SERVICE_CHECK_COUNT = 30;
72 private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
74 private final UpnpIOService upnpIOService;
75 private final DiscoveryServiceRegistry discoveryServiceRegistry;
76 private final UpnpService upnpService;
77 private final WebSocketFactory webSocketFactory;
79 private SamsungTvConfiguration configuration;
81 private @Nullable ThingUID upnpThingUID = null;
83 /* Samsung TV services */
84 private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
86 /* Store powerState to be able to restore upon new link */
87 private boolean powerState = false;
89 /* Store if art mode is supported to be able to skip switching power state to ON during initialization */
90 boolean artModeIsSupported = false;
92 private @Nullable ScheduledFuture<?> pollingJob;
94 public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, DiscoveryServiceRegistry discoveryServiceRegistry,
95 UpnpService upnpService, WebSocketFactory webSocketFactory) {
98 logger.debug("Create a Samsung TV Handler for thing '{}'", getThing().getUID());
100 this.upnpIOService = upnpIOService;
101 this.upnpService = upnpService;
102 this.discoveryServiceRegistry = discoveryServiceRegistry;
103 this.webSocketFactory = webSocketFactory;
104 this.configuration = getConfigAs(SamsungTvConfiguration.class);
108 public void handleCommand(ChannelUID channelUID, Command command) {
109 logger.debug("Received channel: {}, command: {}", channelUID, command);
111 String channel = channelUID.getId();
113 // if power on command try WOL for good measure:
114 if ((channel.equals(POWER) || channel.equals(ART_MODE)) && OnOffType.ON.equals(command)) {
115 sendWOLandResendCommand(channel, command);
118 // Delegate command to correct service
119 for (SamsungTvService service : services) {
120 for (String s : service.getSupportedChannelNames()) {
121 if (channel.equals(s)) {
122 service.handleCommand(channel, command);
128 logger.warn("Channel '{}' not supported", channelUID);
132 public void channelLinked(ChannelUID channelUID) {
133 logger.trace("channelLinked: {}", channelUID);
135 updateState(POWER, getPowerState() ? OnOffType.ON : OnOffType.OFF);
137 for (SamsungTvService service : services) {
138 service.clearCache();
142 private synchronized void setPowerState(boolean state) {
146 private synchronized boolean getPowerState() {
151 public void initialize() {
152 updateStatus(ThingStatus.UNKNOWN);
154 logger.debug("Initializing Samsung TV handler for uid '{}'", getThing().getUID());
156 configuration = getConfigAs(SamsungTvConfiguration.class);
158 discoveryServiceRegistry.addDiscoveryListener(this);
160 checkAndCreateServices();
162 logger.debug("Start refresh task, interval={}", configuration.refreshInterval);
163 pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, configuration.refreshInterval,
164 TimeUnit.MILLISECONDS);
168 public void dispose() {
169 logger.debug("Disposing SamsungTvHandler");
171 if (pollingJob != null) {
172 if (!pollingJob.isCancelled()) {
173 pollingJob.cancel(true);
178 discoveryServiceRegistry.removeDiscoveryListener(this);
183 private void shutdown() {
184 logger.debug("Shutdown all Samsung services");
185 for (SamsungTvService service : services) {
186 stopService(service);
191 private synchronized void putOnline() {
193 updateStatus(ThingStatus.ONLINE);
195 if (!artModeIsSupported) {
196 updateState(POWER, OnOffType.ON);
200 private synchronized void putOffline() {
201 setPowerState(false);
202 updateStatus(ThingStatus.OFFLINE);
203 updateState(ART_MODE, OnOffType.OFF);
204 updateState(POWER, OnOffType.OFF);
205 updateState(SOURCE_APP, new StringType(""));
208 private void poll() {
209 for (SamsungTvService service : services) {
210 for (String channel : service.getSupportedChannelNames()) {
211 if (isLinked(channel)) {
212 // Avoid redundant REFRESH commands when 2 channels are linked to the same UPnP action request
213 if ((channel.equals(SOURCE_ID) && isLinked(SOURCE_NAME))
214 || (channel.equals(CHANNEL_NAME) && isLinked(PROGRAM_TITLE))) {
217 service.handleCommand(channel, RefreshType.REFRESH);
224 public synchronized void valueReceived(String variable, State value) {
225 logger.debug("Received value '{}':'{}' for thing '{}'", variable, value, this.getThing().getUID());
227 if (POWER.equals(variable)) {
228 setPowerState(OnOffType.ON.equals(value));
229 } else if (ART_MODE.equals(variable)) {
230 artModeIsSupported = true;
232 updateState(variable, value);
236 public void reportError(ThingStatusDetail statusDetail, @Nullable String message, @Nullable Throwable e) {
237 logger.debug("Error was reported: {}", message, e);
238 updateStatus(ThingStatus.OFFLINE, statusDetail, message);
242 * One Samsung TV contains several UPnP devices. Samsung TV is discovered by
243 * Media Renderer UPnP device. This function tries to find another UPnP
244 * devices related to same Samsung TV and create handler for those.
246 private void checkAndCreateServices() {
247 logger.debug("Check and create missing UPnP services");
249 for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
250 createService((RemoteDevice) device);
253 checkCreateManualConnection();
256 private synchronized void createService(RemoteDevice device) {
257 if (configuration.hostName != null
258 && configuration.hostName.equals(device.getIdentity().getDescriptorURL().getHost())) {
259 String modelName = device.getDetails().getModelDetails().getModelName();
260 String udn = device.getIdentity().getUdn().getIdentifierString();
261 String type = device.getType().getType();
263 SamsungTvService existingService = findServiceInstance(type);
265 if (existingService == null || !existingService.isUpnp()) {
266 SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
267 configuration.hostName, configuration.port);
269 if (newService != null) {
270 if (existingService != null) {
271 stopService(existingService);
272 startService(newService);
273 logger.debug("Restarting service in UPnP mode for: {}, {} ({})", modelName, type, udn);
275 startService(newService);
276 logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
279 logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
282 logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
283 existingService.clearCache();
289 private @Nullable SamsungTvService findServiceInstance(String serviceName) {
290 Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
292 for (SamsungTvService service : services) {
293 if (service.getClass() == cl) {
300 private synchronized void checkCreateManualConnection() {
302 // create remote service manually if it does not yet exist
304 RemoteControllerService service = (RemoteControllerService) findServiceInstance(
305 RemoteControllerService.SERVICE_NAME);
306 if (service == null) {
307 service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
308 startService(service);
310 // open connection again if needed
311 if (!service.checkConnection()) {
315 } catch (RuntimeException e) {
316 logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
320 private synchronized void startService(SamsungTvService service) {
321 service.addEventListener(this);
323 services.add(service);
326 private synchronized void stopService(SamsungTvService service) {
328 service.removeEventListener(this);
329 services.remove(service);
333 public void thingDiscovered(DiscoveryService source, DiscoveryResult result) {
334 if (configuration.hostName != null
335 && configuration.hostName.equals(result.getProperties().get(SamsungTvConfiguration.HOST_NAME))) {
336 logger.debug("thingDiscovered: {}, {}", result.getProperties().get(SamsungTvConfiguration.HOST_NAME),
339 /* Check if configuration should be updated */
340 if (configuration.macAddress == null || configuration.macAddress.trim().isEmpty()) {
341 String macAddress = WakeOnLanUtility.getMACAddress(configuration.hostName);
342 if (macAddress != null) {
343 putConfig(SamsungTvConfiguration.MAC_ADDRESS, macAddress);
344 logger.debug("thingDiscovered, macAddress: {}", macAddress);
347 if (SamsungTvConfiguration.PROTOCOL_NONE.equals(configuration.protocol)) {
348 Map<String, Object> properties = RemoteControllerService.discover(configuration.hostName);
349 for (Map.Entry<String, Object> property : properties.entrySet()) {
350 putConfig(property.getKey(), property.getValue());
351 logger.debug("thingDiscovered, {}: {}", property.getKey(), property.getValue());
356 * SamsungTV discovery services creates thing UID from UPnP UDN.
357 * When thing is generated manually, thing UID may not match UPnP UDN, so store it for later use (e.g.
360 upnpThingUID = result.getThingUID();
361 logger.debug("thingDiscovered, thingUID={}, discoveredUID={}", this.getThing().getUID(), upnpThingUID);
362 checkAndCreateServices();
367 public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
368 if (thingUID.equals(upnpThingUID)) {
369 logger.debug("Thing Removed: {}", thingUID);
376 public @Nullable Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp,
377 @Nullable Collection<ThingTypeUID> thingTypeUIDs, @Nullable ThingUID bridgeUID) {
378 return Collections.emptyList();
382 * Send multiple WOL packets spaced with 100ms intervals and resend command
384 * @param channel Channel to resend command on
385 * @param command Command to resend
387 private void sendWOLandResendCommand(String channel, Command command) {
388 if (configuration.macAddress == null || configuration.macAddress.isEmpty()) {
389 logger.warn("Cannot send WOL packet to {} MAC address unknown", configuration.hostName);
392 logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
394 // send max 10 WOL packets with 100ms intervals
395 scheduler.schedule(new Runnable() {
401 if (count < WOL_PACKET_RETRY_COUNT) {
402 WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
403 scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
406 }, 1, TimeUnit.MILLISECONDS);
408 // after RemoteService up again to ensure state is properly set
409 scheduler.schedule(new Runnable() {
415 if (count < WOL_SERVICE_CHECK_COUNT) {
416 RemoteControllerService service = (RemoteControllerService) findServiceInstance(
417 RemoteControllerService.SERVICE_NAME);
418 if (service != null) {
419 logger.info("Service found after {} attempts: resend command {} to channel {}", count,
421 service.handleCommand(channel, command);
423 scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
426 logger.info("Service NOT found after {} attempts", count);
429 }, 1000, TimeUnit.MILLISECONDS);
434 public void putConfig(@Nullable String key, @Nullable Object value) {
435 getConfig().put(key, value);
436 configuration = getConfigAs(SamsungTvConfiguration.class);
440 public Object getConfig(@Nullable String key) {
441 return getConfig().get(key);
445 public WebSocketFactory getWebSocketFactory() {
446 return webSocketFactory;