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