]> git.basschouten.com Git - openhab-addons.git/blob
db4c95e4278d30bbe677d6053feaad5c3fc3e13d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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, OnOffType.from(getPowerState()));
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         boolean isOnline = false;
243
244         for (Device<?, ?, ?> device : upnpService.getRegistry().getDevices()) {
245             if (createService((RemoteDevice) device)) {
246                 isOnline = true;
247             }
248         }
249
250         if (isOnline) {
251             logger.debug("Device was online");
252             putOnline();
253         } else {
254             logger.debug("Device was NOT online");
255             putOffline();
256         }
257
258         checkCreateManualConnection();
259     }
260
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();
267
268             SamsungTvService existingService = findServiceInstance(type);
269
270             if (existingService == null || !existingService.isUpnp()) {
271                 SamsungTvService newService = ServiceFactory.createService(type, upnpIOService, udn,
272                         configuration.hostName, configuration.port);
273
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);
279                     } else {
280                         startService(newService);
281                         logger.debug("Started service for: {}, {} ({})", modelName, type, udn);
282                     }
283                 } else {
284                     logger.trace("Skipping unknown UPnP service: {}, {} ({})", modelName, type, udn);
285                 }
286             } else {
287                 logger.debug("Service rediscovered, clearing caches: {}, {} ({})", modelName, type, udn);
288                 existingService.clearCache();
289             }
290             return true;
291         }
292         return false;
293     }
294
295     private @Nullable SamsungTvService findServiceInstance(String serviceName) {
296         Class<? extends SamsungTvService> cl = ServiceFactory.getClassByServiceName(serviceName);
297
298         for (SamsungTvService service : services) {
299             if (service.getClass() == cl) {
300                 return service;
301             }
302         }
303         return null;
304     }
305
306     private synchronized void checkCreateManualConnection() {
307         try {
308             // create remote service manually if it does not yet exist
309
310             RemoteControllerService service = (RemoteControllerService) findServiceInstance(
311                     RemoteControllerService.SERVICE_NAME);
312             if (service == null) {
313                 service = RemoteControllerService.createNonUpnpService(configuration.hostName, configuration.port);
314                 startService(service);
315             } else {
316                 // open connection again if needed
317                 if (!service.checkConnection()) {
318                     service.start();
319                 }
320             }
321         } catch (RuntimeException e) {
322             logger.warn("Catching all exceptions because otherwise the thread would silently fail", e);
323         }
324     }
325
326     private synchronized void startService(SamsungTvService service) {
327         service.addEventListener(this);
328         service.start();
329         services.add(service);
330     }
331
332     private synchronized void stopService(SamsungTvService service) {
333         service.stop();
334         service.removeEventListener(this);
335         services.remove(service);
336     }
337
338     @Override
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());
346
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);
353                 }
354             }
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());
360                 }
361             }
362             upnpUDN = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
363             logger.debug("remoteDeviceAdded, upnpUDN={}", upnpUDN);
364             checkAndCreateServices();
365         }
366     }
367
368     @Override
369     public void remoteDeviceRemoved(@Nullable Registry registry, @Nullable RemoteDevice device) {
370         if (device == null) {
371             return;
372         }
373         String udn = device.getIdentity().getUdn().getIdentifierString().replace("-", "_");
374         if (udn.equals(upnpUDN)) {
375             logger.debug("Device removed: udn={}", upnpUDN);
376             shutdown();
377             putOffline();
378             checkCreateManualConnection();
379         }
380     }
381
382     @Override
383     public void remoteDeviceUpdated(@Nullable Registry registry, @Nullable RemoteDevice device) {
384     }
385
386     @Override
387     public void remoteDeviceDiscoveryStarted(@Nullable Registry registry, @Nullable RemoteDevice device) {
388     }
389
390     @Override
391     public void remoteDeviceDiscoveryFailed(@Nullable Registry registry, @Nullable RemoteDevice device,
392             @Nullable Exception ex) {
393     }
394
395     @Override
396     public void localDeviceAdded(@Nullable Registry registry, @Nullable LocalDevice device) {
397     }
398
399     @Override
400     public void localDeviceRemoved(@Nullable Registry registry, @Nullable LocalDevice device) {
401     }
402
403     @Override
404     public void beforeShutdown(@Nullable Registry registry) {
405     }
406
407     @Override
408     public void afterShutdown() {
409     }
410
411     /**
412      * Send multiple WOL packets spaced with 100ms intervals and resend command
413      *
414      * @param channel Channel to resend command on
415      * @param command Command to resend
416      */
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);
420             return;
421         } else {
422             logger.info("Send WOL packet to {} ({})", configuration.hostName, configuration.macAddress);
423
424             // send max 10 WOL packets with 100ms intervals
425             scheduler.schedule(new Runnable() {
426                 int count = 0;
427
428                 @Override
429                 public void run() {
430                     count++;
431                     if (count < WOL_PACKET_RETRY_COUNT) {
432                         WakeOnLanUtility.sendWOLPacket(configuration.macAddress);
433                         scheduler.schedule(this, 100, TimeUnit.MILLISECONDS);
434                     }
435                 }
436             }, 1, TimeUnit.MILLISECONDS);
437
438             // after RemoteService up again to ensure state is properly set
439             scheduler.schedule(new Runnable() {
440                 int count = 0;
441
442                 @Override
443                 public void run() {
444                     count++;
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,
450                                     command, channel);
451                             service.handleCommand(channel, command);
452                         } else {
453                             scheduler.schedule(this, 1000, TimeUnit.MILLISECONDS);
454                         }
455                     } else {
456                         logger.info("Service NOT found after {} attempts", count);
457                     }
458                 }
459             }, 1000, TimeUnit.MILLISECONDS);
460         }
461     }
462
463     @Override
464     public void putConfig(@Nullable String key, @Nullable Object value) {
465         getConfig().put(key, value);
466         configuration = getConfigAs(SamsungTvConfiguration.class);
467     }
468
469     @Override
470     public Object getConfig(@Nullable String key) {
471         return getConfig().get(key);
472     }
473
474     @Override
475     public WebSocketFactory getWebSocketFactory() {
476         return webSocketFactory;
477     }
478 }