]> git.basschouten.com Git - openhab-addons.git/blob
7e69a1839e5b2ce56d59bcc069a874f48d99db9e
[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.service;
14
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
16
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.stream.Collectors;
23 import java.util.stream.IntStream;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.samsungtv.internal.Utils;
28 import org.openhab.binding.samsungtv.internal.handler.SamsungTvHandler;
29 import org.openhab.binding.samsungtv.internal.service.api.SamsungTvService;
30 import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
31 import org.openhab.core.io.transport.upnp.UpnpIOService;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.types.Command;
36 import org.openhab.core.types.RefreshType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39 import org.w3c.dom.Element;
40 import org.w3c.dom.Node;
41
42 /**
43  * The {@link MainTVServerService} is responsible for handling MainTVServer
44  * commands.
45  *
46  * @author Pauli Anttila - Initial contribution
47  * @author Nick Waterton - add checkConnection(), getServiceName, some refactoring
48  */
49 @NonNullByDefault
50 public class MainTVServerService implements UpnpIOParticipant, SamsungTvService {
51
52     public static final String SERVICE_NAME = "MainTVServer2";
53     private static final String SERVICE_MAIN_AGENT = "MainTVAgent2";
54     private static final List<String> SUPPORTED_CHANNELS = List.of(SOURCE_NAME, SOURCE_ID, BROWSER_URL, STOP_BROWSER);
55     private static final List<String> REFRESH_CHANNELS = List.of(CHANNEL, SOURCE_NAME, SOURCE_ID, PROGRAM_TITLE,
56             CHANNEL_NAME, BROWSER_URL);
57     private static final List<String> SUBSCRIPTION_REFRESH_CHANNELS = List.of(SOURCE_NAME);
58     protected static final int SUBSCRIPTION_DURATION = 1800;
59     private final Logger logger = LoggerFactory.getLogger(MainTVServerService.class);
60
61     private final UpnpIOService service;
62
63     private final String udn;
64     private String host = "";
65
66     private final SamsungTvHandler handler;
67
68     private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
69     private Map<String, String> sources = Collections.synchronizedMap(new HashMap<>());
70
71     private boolean started;
72     private boolean subscription;
73
74     public MainTVServerService(UpnpIOService upnpIOService, String udn, String host, SamsungTvHandler handler) {
75         this.service = upnpIOService;
76         this.udn = udn;
77         this.handler = handler;
78         this.host = host;
79         logger.debug("{}: Creating a Samsung TV MainTVServer service: subscription={}", host, getSubscription());
80     }
81
82     private boolean getSubscription() {
83         return handler.configuration.getSubscription();
84     }
85
86     @Override
87     public String getServiceName() {
88         return SERVICE_NAME;
89     }
90
91     @Override
92     public List<String> getSupportedChannelNames(boolean refresh) {
93         if (refresh) {
94             if (subscription) {
95                 return SUBSCRIPTION_REFRESH_CHANNELS;
96             }
97             return REFRESH_CHANNELS;
98         }
99         logger.trace("{}: getSupportedChannelNames: {}", host, SUPPORTED_CHANNELS);
100         return SUPPORTED_CHANNELS;
101     }
102
103     @Override
104     public void start() {
105         service.registerParticipant(this);
106         addSubscription();
107         started = true;
108     }
109
110     @Override
111     public void stop() {
112         removeSubscription();
113         service.unregisterParticipant(this);
114         started = false;
115     }
116
117     @Override
118     public void clearCache() {
119         stateMap.clear();
120         sources.clear();
121     }
122
123     @Override
124     public boolean isUpnp() {
125         return true;
126     }
127
128     @Override
129     public boolean checkConnection() {
130         return started;
131     }
132
133     @Override
134     public boolean handleCommand(String channel, Command command) {
135         logger.trace("{}: Received channel: {}, command: {}", host, channel, command);
136         boolean result = false;
137
138         if (!checkConnection()) {
139             return false;
140         }
141
142         if (command == RefreshType.REFRESH) {
143             if (isRegistered()) {
144                 switch (channel) {
145                     case CHANNEL:
146                         updateResourceState("GetCurrentMainTVChannel");
147                         break;
148                     case SOURCE_NAME:
149                     case SOURCE_ID:
150                         updateResourceState("GetCurrentExternalSource");
151                         break;
152                     case PROGRAM_TITLE:
153                     case CHANNEL_NAME:
154                         updateResourceState("GetCurrentContentRecognition");
155                         break;
156                     case BROWSER_URL:
157                         updateResourceState("GetCurrentBrowserURL");
158                         break;
159                     default:
160                         break;
161                 }
162             }
163             return true;
164         }
165
166         switch (channel) {
167             case SOURCE_ID:
168                 if (command instanceof DecimalType) {
169                     command = new StringType(command.toString());
170                 }
171             case SOURCE_NAME:
172                 if (command instanceof StringType) {
173                     result = setSourceName(command);
174                     updateResourceState("GetCurrentExternalSource");
175                 }
176                 break;
177             case BROWSER_URL:
178                 if (command instanceof StringType) {
179                     result = setBrowserUrl(command);
180                 }
181                 break;
182             case STOP_BROWSER:
183                 if (command instanceof OnOffType) {
184                     // stop browser if command is On or Off
185                     result = stopBrowser();
186                     if (result) {
187                         onValueReceived("BrowserURL", "", SERVICE_MAIN_AGENT);
188                     }
189                 }
190                 break;
191             default:
192                 logger.warn("{}: Samsung TV doesn't support send for channel '{}'", host, channel);
193                 return false;
194         }
195         if (!result) {
196             logger.warn("{}: main tvservice: command error {} channel {}", host, command, channel);
197         }
198         return result;
199     }
200
201     private boolean isRegistered() {
202         return service.isRegistered(this);
203     }
204
205     @Override
206     public String getUDN() {
207         return udn;
208     }
209
210     private void addSubscription() {
211         // Set up GENA Subscriptions
212         if (isRegistered() && getSubscription()) {
213             logger.debug("{}: Subscribing to service {}...", host, SERVICE_MAIN_AGENT);
214             service.addSubscription(this, SERVICE_MAIN_AGENT, SUBSCRIPTION_DURATION);
215         }
216     }
217
218     private void removeSubscription() {
219         // Remove GENA Subscriptions
220         if (isRegistered() && subscription) {
221             logger.debug("{}: Unsubscribing from service {}...", host, SERVICE_MAIN_AGENT);
222             service.removeSubscription(this, SERVICE_MAIN_AGENT);
223         }
224     }
225
226     @Override
227     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
228         if (service == null) {
229             return;
230         }
231         subscription = succeeded;
232         logger.debug("{}: Subscription to service {} {}", host, service, succeeded ? "succeeded" : "failed");
233     }
234
235     @Override
236     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
237         if (variable == null || value == null || service == null || variable.isBlank()) {
238             return;
239         }
240
241         variable = variable.replace("Current", "");
242         String oldValue = stateMap.getOrDefault(variable, "None");
243         if (value.equals(oldValue)) {
244             logger.trace("{}: Value '{}' for {} hasn't changed, ignoring update", host, value, variable);
245             return;
246         }
247
248         stateMap.put(variable, value);
249
250         switch (variable) {
251             case "A_ARG_TYPE_LastChange":
252                 parseEventValues(value);
253                 break;
254             case "ProgramTitle":
255                 handler.valueReceived(PROGRAM_TITLE, new StringType(value));
256                 break;
257             case "ChannelName":
258                 handler.valueReceived(CHANNEL_NAME, new StringType(value));
259                 break;
260             case "ExternalSource":
261                 handler.valueReceived(SOURCE_NAME, new StringType(value));
262                 break;
263             case "MajorCh":
264                 handler.valueReceived(CHANNEL, new DecimalType(value));
265                 break;
266             case "ID":
267                 handler.valueReceived(SOURCE_ID, new DecimalType(value));
268                 break;
269             case "BrowserURL":
270                 handler.valueReceived(BROWSER_URL, new StringType(value));
271                 break;
272         }
273     }
274
275     protected Map<String, String> updateResourceState(String actionId) {
276         return updateResourceState(actionId, Map.of());
277     }
278
279     protected synchronized Map<String, String> updateResourceState(String actionId, Map<String, String> inputs) {
280         Map<String, String> result = Optional.of(service)
281                 .map(a -> a.invokeAction(this, SERVICE_MAIN_AGENT, actionId, inputs)).filter(a -> !a.isEmpty())
282                 .orElse(Map.of("Result", "Command Failed"));
283         if (isOk(result)) {
284             result.keySet().stream().filter(a -> !"Result".equals(a)).forEach(a -> {
285                 String val = result.getOrDefault(a, "");
286                 if ("CurrentChannel".equals(a)) {
287                     val = parseCurrentChannel(val);
288                     a = "MajorCh";
289                 }
290                 onValueReceived(a, val, SERVICE_MAIN_AGENT);
291             });
292         }
293         return result;
294     }
295
296     public boolean isOk(Map<String, String> result) {
297         return result.getOrDefault("Result", "Error").equals("OK");
298     }
299
300     /**
301      * Searches sources for source, or ID, and sets TV input to that value
302      */
303     private boolean setSourceName(Command command) {
304         String tmpSource = command.toString();
305         if (sources.isEmpty()) {
306             getSourceMap();
307         }
308         String source = sources.entrySet().stream().filter(a -> a.getValue().equals(tmpSource)).map(a -> a.getKey())
309                 .findFirst().orElse(tmpSource);
310         Map<String, String> result = updateResourceState("SetMainTVSource",
311                 Map.of("Source", source, "ID", sources.getOrDefault(source, "0"), "UiID", "0"));
312         logResult(result.getOrDefault("Result", "Unable to Set Source Name: " + source));
313         return isOk(result);
314     }
315
316     private boolean setBrowserUrl(Command command) {
317         Map<String, String> result = updateResourceState("RunBrowser", Map.of("BrowserURL", command.toString()));
318         logResult(result.getOrDefault("Result", "Unable to Set browser URL: " + command.toString()));
319         return isOk(result);
320     }
321
322     private boolean stopBrowser() {
323         Map<String, String> result = updateResourceState("StopBrowser");
324         logResult(result.getOrDefault("Result", "Unable to Stop Browser"));
325         return isOk(result);
326     }
327
328     private void logResult(String ok) {
329         if ("OK".equals(ok)) {
330             logger.debug("{}: Command successfully executed", host);
331         } else {
332             logger.warn("{}: Command execution failed, result='{}'", host, ok);
333         }
334     }
335
336     private String parseCurrentChannel(String xml) {
337         return Utils.loadXMLFromString(xml, host).map(a -> a.getDocumentElement())
338                 .map(a -> getFirstNodeValue(a, "MajorCh", "-1")).orElse("-1");
339     }
340
341     private void getSourceMap() {
342         // NodeList doesn't have a stream, so do this
343         sources = Optional.of(updateResourceState("GetSourceList")).filter(a -> "OK".equals(a.get("Result")))
344                 .map(a -> a.get("SourceList")).flatMap(xml -> Utils.loadXMLFromString(xml, host))
345                 .map(a -> a.getDocumentElement()).map(a -> a.getElementsByTagName("Source"))
346                 .map(nList -> IntStream.range(0, nList.getLength()).boxed().map(i -> (Element) nList.item(i))
347                         .collect(Collectors.toMap(a -> getFirstNodeValue(a, "SourceType", ""),
348                                 a -> getFirstNodeValue(a, "ID", ""))))
349                 .orElse(Map.of());
350     }
351
352     private String getFirstNodeValue(Element nodeList, String node, String ifNone) {
353         return Optional.ofNullable(nodeList).map(a -> a.getElementsByTagName(node)).filter(a -> a.getLength() > 0)
354                 .map(a -> a.item(0)).map(a -> a.getTextContent()).orElse(ifNone);
355     }
356
357     /**
358      * Parse Subscription Event from {@link String} which contains XML content.
359      * Parses all child Nodes recursively.
360      * If valid channel update is found, call onValueReceived()
361      *
362      * @param xml{@link String} which contains XML content.
363      */
364     public void parseEventValues(String xml) {
365         Utils.loadXMLFromString(xml, host).ifPresent(a -> visitRecursively(a));
366     }
367
368     public void visitRecursively(Node node) {
369         // get all child nodes, NodeList doesn't have a stream, so do this
370         Optional.ofNullable(node.getChildNodes()).ifPresent(nList -> IntStream.range(0, nList.getLength())
371                 .mapToObj(i -> (Node) nList.item(i)).forEach(childNode -> parseNode(childNode)));
372     }
373
374     public void parseNode(Node node) {
375         if (node.getNodeType() == Node.ELEMENT_NODE) {
376             Element el = (Element) node;
377             switch (el.getNodeName()) {
378                 case "BrowserChanged":
379                     if ("Disable".equals(el.getTextContent())) {
380                         onValueReceived("BrowserURL", "", SERVICE_MAIN_AGENT);
381                     } else {
382                         updateResourceState("GetCurrentBrowserURL");
383                     }
384                     break;
385                 case "PowerOFF":
386                     logger.debug("{}: TV has Powered Off", host);
387                     handler.setOffline();
388                     break;
389                 case "MajorCh":
390                 case "ChannelName":
391                 case "ProgramTitle":
392                 case "ExternalSource":
393                 case "ID":
394                 case "BrowserURL":
395                     logger.trace("{}: Processing {}:{}", host, el.getNodeName(), el.getTextContent());
396                     onValueReceived(el.getNodeName(), el.getTextContent(), SERVICE_MAIN_AGENT);
397                     break;
398             }
399         }
400         // visit child node
401         visitRecursively(node);
402     }
403
404     @Override
405     public void onStatusChanged(boolean status) {
406         logger.trace("{}: onStatusChanged: status={}", host, status);
407         if (!status) {
408             handler.setOffline();
409         }
410     }
411 }