]> git.basschouten.com Git - openhab-addons.git/blob
8c09e86d019d94df95ea1d4ccad3cb2dc0243606
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.Map;
18 import java.util.Set;
19 import java.util.concurrent.CopyOnWriteArraySet;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
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;
51
52 /**
53  * The {@link SamsungTvHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
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
60  */
61 @NonNullByDefault
62 public class SamsungTvHandler extends BaseThingHandler implements RegistryListener, EventListener {
63
64     private static final int WOL_PACKET_RETRY_COUNT = 10;
65     private static final int WOL_SERVICE_CHECK_COUNT = 30;
66
67     private final Logger logger = LoggerFactory.getLogger(SamsungTvHandler.class);
68
69     private final UpnpIOService upnpIOService;
70     private final UpnpService upnpService;
71     private final WebSocketFactory webSocketFactory;
72
73     private SamsungTvConfiguration configuration;
74
75     private @Nullable String upnpUDN = null;
76
77     /* Samsung TV services */
78     private final Set<SamsungTvService> services = new CopyOnWriteArraySet<>();
79
80     /* Store powerState to be able to restore upon new link */
81     private boolean powerState = false;
82
83     /* Store if art mode is supported to be able to skip switching power state to ON during initialization */
84     boolean artModeIsSupported = false;
85
86     private @Nullable ScheduledFuture<?> pollingJob;
87
88     public SamsungTvHandler(Thing thing, UpnpIOService upnpIOService, UpnpService upnpService,
89             WebSocketFactory webSocketFactory) {
90         super(thing);
91
92         logger.debug("Create a Samsung TV Handler for thing '{}'", getThing().getUID());
93
94         this.upnpIOService = upnpIOService;
95         this.upnpService = upnpService;
96         this.webSocketFactory = webSocketFactory;
97         this.configuration = getConfigAs(SamsungTvConfiguration.class);
98     }
99
100     @Override
101     public void handleCommand(ChannelUID channelUID, Command command) {
102         logger.debug("Received channel: {}, command: {}", channelUID, command);
103
104         String channel = channelUID.getId();
105
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);
109         }
110
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);
116                     return;
117                 }
118             }
119         }
120
121         logger.warn("Channel '{}' not supported", channelUID);
122     }
123
124     @Override
125     public void channelLinked(ChannelUID channelUID) {
126         logger.trace("channelLinked: {}", channelUID);
127
128         updateState(POWER, getPowerState() ? OnOffType.ON : OnOffType.OFF);
129
130         for (SamsungTvService service : services) {
131             service.clearCache();
132         }
133     }
134
135     private synchronized void setPowerState(boolean state) {
136         powerState = state;
137     }
138
139     private synchronized boolean getPowerState() {
140         return powerState;
141     }
142
143     @Override
144     public void initialize() {
145         updateStatus(ThingStatus.UNKNOWN);
146
147         logger.debug("Initializing Samsung TV handler for uid '{}'", getThing().getUID());
148
149         configuration = getConfigAs(SamsungTvConfiguration.class);
150
151         upnpService.getRegistry().addListener(this);
152
153         checkAndCreateServices();
154
155         logger.debug("Start refresh task, interval={}", configuration.refreshInterval);
156         pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, configuration.refreshInterval,
157                 TimeUnit.MILLISECONDS);
158     }
159
160     @Override
161     public void dispose() {
162         logger.debug("Disposing SamsungTvHandler");
163
164         if (pollingJob != null) {
165             if (!pollingJob.isCancelled()) {
166                 pollingJob.cancel(true);
167             }
168             pollingJob = null;
169         }
170
171         upnpService.getRegistry().removeListener(this);
172         shutdown();
173         putOffline();
174     }
175
176     private void shutdown() {
177         logger.debug("Shutdown all Samsung services");
178         for (SamsungTvService service : services) {
179             stopService(service);
180         }
181         services.clear();
182     }
183
184     private synchronized void putOnline() {
185         setPowerState(true);
186         updateStatus(ThingStatus.ONLINE);
187
188         if (!artModeIsSupported) {
189             updateState(POWER, OnOffType.ON);
190         }
191     }
192
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(""));
199     }
200
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))) {
208                         continue;
209                     }
210                     service.handleCommand(channel, RefreshType.REFRESH);
211                 }
212             }
213         }
214     }
215
216     @Override
217     public synchronized void valueReceived(String variable, State value) {
218         logger.debug("Received value '{}':'{}' for thing '{}'", variable, value, this.getThing().getUID());
219
220         if (POWER.equals(variable)) {
221             setPowerState(OnOffType.ON.equals(value));
222         } else if (ART_MODE.equals(variable)) {
223             artModeIsSupported = true;
224         }
225         updateState(variable, value);
226     }
227
228     @Override
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);
232     }
233
234     /**
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.
238      */
239     private void checkAndCreateServices() {
240         logger.debug("Check and create missing UPnP services");
241
242         for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
243             createService((RemoteDevice) device);
244         }
245
246         checkCreateManualConnection();
247     }
248
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();
255
256             SamsungTvService existingService = findServiceInstance(type);
257
258             if (existingService == null || !existingService.isUpnp()) {
259                 SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
260                         configuration.hostName, configuration.port);
261
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);
267                     } else {
268                         startService(newService);
269                         logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
270                     }
271                 } else {
272                     logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
273                 }
274             } else {
275                 logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
276                 existingService.clearCache();
277             }
278             putOnline();
279         }
280     }
281
282     private @Nullable SamsungTvService findServiceInstance(String serviceName) {
283         Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
284
285         for (SamsungTvService service : services) {
286             if (service.getClass() == cl) {
287                 return service;
288             }
289         }
290         return null;
291     }
292
293     private synchronized void checkCreateManualConnection() {
294         try {
295             // create remote service manually if it does not yet exist
296
297             RemoteControllerService service = (RemoteControllerService) findServiceInstance(
298                     RemoteControllerService.SERVICE_NAME);
299             if (service == null) {
300                 service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
301                 startService(service);
302             } else {
303                 // open connection again if needed
304                 if (!service.checkConnection()) {
305                     service.start();
306                 }
307             }
308         } catch (RuntimeException e) {
309             logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
310         }
311     }
312
313     private synchronized void startService(SamsungTvService service) {
314         service.addEventListener(this);
315         service.start();
316         services.add(service);
317     }
318
319     private synchronized void stopService(SamsungTvService service) {
320         service.stop();
321         service.removeEventListener(this);
322         services.remove(service);
323     }
324
325     @Override
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());
333
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);
340                 }
341             }
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());
347                 }
348             }
349             upnpUDN = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
350             logger.debug("remoteDeviceAdded, upnpUDN={}", upnpUDN);
351             checkAndCreateServices();
352         }
353     }
354
355     @Override
356     public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
357         if (device == null) {
358             return;
359         }
360         String udn = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
361         if (udn.equals(upnpUDN)) {
362             logger.debug("Device removed: udn={}", upnpUDN);
363             shutdown();
364             putOffline();
365         }
366     }
367
368     @Override
369     public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
370     }
371
372     @Override
373     public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
374     }
375
376     @Override
377     public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
378             @Nullable Exception ex) {
379     }
380
381     @Override
382     public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
383     }
384
385     @Override
386     public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
387     }
388
389     @Override
390     public void beforeShutdown(@Nullable Registry registry) {
391     }
392
393     @Override
394     public void afterShutdown() {
395     }
396
397     /**
398      * Send multiple WOL packets spaced with 100ms intervals and resend command
399      *
400      * @param channel Channel to resend command on
401      * @param command Command to resend
402      */
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);
406             return;
407         } else {
408             logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
409
410             // send max 10 WOL packets with 100ms intervals
411             scheduler.schedule(new Runnable() {
412                 int count = 0;
413
414                 @Override
415                 public void run() {
416                     count++;
417                     if (count < WOL_PACKET_RETRY_COUNT) {
418                         WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
419                         scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
420                     }
421                 }
422             }, 1, TimeUnit.MILLISECONDS);
423
424             // after RemoteService up again to ensure state is properly set
425             scheduler.schedule(new Runnable() {
426                 int count = 0;
427
428                 @Override
429                 public void run() {
430                     count++;
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,
436                                     command, channel);
437                             service.handleCommand(channel, command);
438                         } else {
439                             scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
440                         }
441                     } else {
442                         logger.info("Service NOT found after {} attempts", count);
443                     }
444                 }
445             }, 1000, TimeUnit.MILLISECONDS);
446         }
447     }
448
449     @Override
450     public void putConfig(@Nullable String key, @Nullable Object value) {
451         getConfig().put(key, value);
452         configuration = getConfigAs(SamsungTvConfiguration.class);
453     }
454
455     @Override
456     public Object getConfig(@Nullable String key) {
457         return getConfig().get(key);
458     }
459
460     @Override
461     public WebSocketFactory getWebSocketFactory() {
462         return webSocketFactory;
463     }
464 }