]> git.basschouten.com Git - openhab-addons.git/blob
302071d9b13936e16695fc09a32725b71254b5a8
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.samsungtv.internal.handler;
14
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
16
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.concurrent.CopyOnWriteArraySet;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24
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;
56
57 /**
58  * The {@link SamsungTvHandler} is responsible for handling commands, which are
59  * sent to one of the channels.
60  *
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
65  */
66 @NonNullByDefault
67 public class SamsungTvHandler extends BaseThingHandler implements DiscoveryListener, EventListener {
68
69     private static final int WOL_PACKET_RETRY_COUNT = 10;
70     private static final int WOL_SERVICE_CHECK_COUNT = 30;
71
72     private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
73
74     private final UpnpIOService upnpIOService;
75     private final DiscoveryServiceRegistry discoveryServiceRegistry;
76     private final UpnpService upnpService;
77     private final WebSocketFactory webSocketFactory;
78
79     private SamsungTvConfiguration configuration;
80
81     private @Nullable ThingUID upnpThingUID = null;
82
83     /* Samsung TV services */
84     private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
85
86     /* Store powerState to be able to restore upon new link */
87     private boolean powerState = false;
88
89     /* Store if art mode is supported to be able to skip switching power state to ON during initialization */
90     boolean artModeIsSupported = false;
91
92     private @Nullable ScheduledFuture<?> pollingJob;
93
94     public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, DiscoveryServiceRegistry discoveryServiceRegistry,
95             UpnpService upnpService, WebSocketFactory webSocketFactory) {
96         super(thing);
97
98         logger.debug("Create a Samsung TV Handler for thing '{}'", getThing().getUID());
99
100         this.upnpIOService = upnpIOService;
101         this.upnpService = upnpService;
102         this.discoveryServiceRegistry = discoveryServiceRegistry;
103         this.webSocketFactory = webSocketFactory;
104         this.configuration = getConfigAs(SamsungTvConfiguration.class);
105     }
106
107     @Override
108     public void handleCommand(ChannelUID channelUID, Command command) {
109         logger.debug("Received channel: {}, command: {}", channelUID, command);
110
111         String channel = channelUID.getId();
112
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);
116         }
117
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);
123                     return;
124                 }
125             }
126         }
127
128         logger.warn("Channel '{}' not supported", channelUID);
129     }
130
131     @Override
132     public void channelLinked(ChannelUID channelUID) {
133         logger.trace("channelLinked: {}", channelUID);
134
135         updateState(POWER, getPowerState() ? OnOffType.ON : OnOffType.OFF);
136
137         for (SamsungTvService service : services) {
138             service.clearCache();
139         }
140     }
141
142     private synchronized void setPowerState(boolean state) {
143         powerState = state;
144     }
145
146     private synchronized boolean getPowerState() {
147         return powerState;
148     }
149
150     @Override
151     public void initialize() {
152         updateStatus(ThingStatus.UNKNOWN);
153
154         logger.debug("Initializing Samsung TV handler for uid '{}'", getThing().getUID());
155
156         configuration = getConfigAs(SamsungTvConfiguration.class);
157
158         discoveryServiceRegistry.addDiscoveryListener(this);
159
160         checkAndCreateServices();
161
162         logger.debug("Start refresh task, interval={}", configuration.refreshInterval);
163         pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, configuration.refreshInterval,
164                 TimeUnit.MILLISECONDS);
165     }
166
167     @Override
168     public void dispose() {
169         logger.debug("Disposing SamsungTvHandler");
170
171         if (pollingJob != null) {
172             if (!pollingJob.isCancelled()) {
173                 pollingJob.cancel(true);
174             }
175             pollingJob = null;
176         }
177
178         discoveryServiceRegistry.removeDiscoveryListener(this);
179         shutdown();
180         putOffline();
181     }
182
183     private void shutdown() {
184         logger.debug("Shutdown all Samsung services");
185         for (SamsungTvService service : services) {
186             stopService(service);
187         }
188         services.clear();
189     }
190
191     private synchronized void putOnline() {
192         setPowerState(true);
193         updateStatus(ThingStatus.ONLINE);
194
195         if (!artModeIsSupported) {
196             updateState(POWER, OnOffType.ON);
197         }
198     }
199
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(""));
206     }
207
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))) {
215                         continue;
216                     }
217                     service.handleCommand(channel, RefreshType.REFRESH);
218                 }
219             }
220         }
221     }
222
223     @Override
224     public synchronized void valueReceived(String variable, State value) {
225         logger.debug("Received value '{}':'{}' for thing '{}'", variable, value, this.getThing().getUID());
226
227         if (POWER.equals(variable)) {
228             setPowerState(OnOffType.ON.equals(value));
229         } else if (ART_MODE.equals(variable)) {
230             artModeIsSupported = true;
231         }
232         updateState(variable, value);
233     }
234
235     @Override
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);
239     }
240
241     /**
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.
245      */
246     private void checkAndCreateServices() {
247         logger.debug("Check and create missing UPnP services");
248
249         for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
250             createService((RemoteDevice) device);
251         }
252
253         checkCreateManualConnection();
254     }
255
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();
262
263             SamsungTvService existingService = findServiceInstance(type);
264
265             if (existingService == null || !existingService.isUpnp()) {
266                 SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
267                         configuration.hostName, configuration.port);
268
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);
274                     } else {
275                         startService(newService);
276                         logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
277                     }
278                 } else {
279                     logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
280                 }
281             } else {
282                 logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
283                 existingService.clearCache();
284             }
285             putOnline();
286         }
287     }
288
289     private @Nullable SamsungTvService findServiceInstance(String serviceName) {
290         Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
291
292         for (SamsungTvService service : services) {
293             if (service.getClass() == cl) {
294                 return service;
295             }
296         }
297         return null;
298     }
299
300     private synchronized void checkCreateManualConnection() {
301         try {
302             // create remote service manually if it does not yet exist
303
304             RemoteControllerService service = (RemoteControllerService) findServiceInstance(
305                     RemoteControllerService.SERVICE_NAME);
306             if (service == null) {
307                 service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
308                 startService(service);
309             } else {
310                 // open connection again if needed
311                 if (!service.checkConnection()) {
312                     service.start();
313                 }
314             }
315         } catch (RuntimeException e) {
316             logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
317         }
318     }
319
320     private synchronized void startService(SamsungTvService service) {
321         service.addEventListener(this);
322         service.start();
323         services.add(service);
324     }
325
326     private synchronized void stopService(SamsungTvService service) {
327         service.stop();
328         service.removeEventListener(this);
329         services.remove(service);
330     }
331
332     @Override
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),
337                     result);
338
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);
345                 }
346             }
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());
352                 }
353             }
354
355             /*
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.
358              * thingRemoved).
359              */
360             upnpThingUID = result.getThingUID();
361             logger.debug("thingDiscovered, thingUID={}, discoveredUID={}", this.getThing().getUID(), upnpThingUID);
362             checkAndCreateServices();
363         }
364     }
365
366     @Override
367     public void thingRemoved(DiscoveryService source, ThingUID thingUID) {
368         if (thingUID.equals(upnpThingUID)) {
369             logger.debug("Thing Removed: {}", thingUID);
370             shutdown();
371             putOffline();
372         }
373     }
374
375     @Override
376     public @Nullable Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp,
377             @Nullable Collection<ThingTypeUID> thingTypeUIDs, @Nullable ThingUID bridgeUID) {
378         return Collections.emptyList();
379     }
380
381     /**
382      * Send multiple WOL packets spaced with 100ms intervals and resend command
383      *
384      * @param channel Channel to resend command on
385      * @param command Command to resend
386      */
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);
390             return;
391         } else {
392             logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
393
394             // send max 10 WOL packets with 100ms intervals
395             scheduler.schedule(new Runnable() {
396                 int count = 0;
397
398                 @Override
399                 public void run() {
400                     count++;
401                     if (count < WOL_PACKET_RETRY_COUNT) {
402                         WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
403                         scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
404                     }
405                 }
406             }, 1, TimeUnit.MILLISECONDS);
407
408             // after RemoteService up again to ensure state is properly set
409             scheduler.schedule(new Runnable() {
410                 int count = 0;
411
412                 @Override
413                 public void run() {
414                     count++;
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,
420                                     command, channel);
421                             service.handleCommand(channel, command);
422                         } else {
423                             scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
424                         }
425                     } else {
426                         logger.info("Service NOT found after {} attempts", count);
427                     }
428                 }
429             }, 1000, TimeUnit.MILLISECONDS);
430         }
431     }
432
433     @Override
434     public void putConfig(@Nullable String key, @Nullable Object value) {
435         getConfig().put(key, value);
436         configuration = getConfigAs(SamsungTvConfiguration.class);
437     }
438
439     @Override
440     public Object getConfig(@Nullable String key) {
441         return getConfig().get(key);
442     }
443
444     @Override
445     public WebSocketFactory getWebSocketFactory() {
446         return webSocketFactory;
447     }
448 }