2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.samsungtv.internal.service;
15 import static org.openhab.binding.samsungtv.internal.SamsungTvBindingConstants.*;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.Optional;
22 import java.util.stream.Collectors;
23 import java.util.stream.IntStream;
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;
43 * The {@link MainTVServerService} is responsible for handling MainTVServer
46 * @author Pauli Anttila - Initial contribution
47 * @author Nick Waterton - add checkConnection(), getServiceName, some refactoring
50 public class MainTVServerService implements UpnpIOParticipant, SamsungTvService {
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);
61 private final UpnpIOService service;
63 private final String udn;
64 private String host = "";
66 private final SamsungTvHandler handler;
68 private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
69 private Map<String, String> sources = Collections.synchronizedMap(new HashMap<>());
71 private boolean started;
72 private boolean subscription;
74 public MainTVServerService(UpnpIOService upnpIOService, String udn, String host, SamsungTvHandler handler) {
75 this.service = upnpIOService;
77 this.handler = handler;
79 logger.debug("{}: Creating a Samsung TV MainTVServer service: subscription={}", host, getSubscription());
82 private boolean getSubscription() {
83 return handler.configuration.getSubscription();
87 public String getServiceName() {
92 public List<String> getSupportedChannelNames(boolean refresh) {
95 return SUBSCRIPTION_REFRESH_CHANNELS;
97 return REFRESH_CHANNELS;
99 logger.trace("{}: getSupportedChannelNames: {}", host, SUPPORTED_CHANNELS);
100 return SUPPORTED_CHANNELS;
104 public void start() {
105 service.registerParticipant(this);
112 removeSubscription();
113 service.unregisterParticipant(this);
118 public void clearCache() {
124 public boolean isUpnp() {
129 public boolean checkConnection() {
134 public boolean handleCommand(String channel, Command command) {
135 logger.trace("{}: Received channel: {}, command: {}", host, channel, command);
136 boolean result = false;
138 if (!checkConnection()) {
142 if (command == RefreshType.REFRESH) {
143 if (isRegistered()) {
146 updateResourceState("GetCurrentMainTVChannel");
150 updateResourceState("GetCurrentExternalSource");
154 updateResourceState("GetCurrentContentRecognition");
157 updateResourceState("GetCurrentBrowserURL");
168 if (command instanceof DecimalType) {
169 command = new StringType(command.toString());
172 if (command instanceof StringType) {
173 result = setSourceName(command);
174 updateResourceState("GetCurrentExternalSource");
178 if (command instanceof StringType) {
179 result = setBrowserUrl(command);
183 if (command instanceof OnOffType) {
184 // stop browser if command is On or Off
185 result = stopBrowser();
187 onValueReceived("BrowserURL", "", SERVICE_MAIN_AGENT);
192 logger.warn("{}: Samsung TV doesn't support send for channel '{}'", host, channel);
196 logger.warn("{}: main tvservice: command error {} channel {}", host, command, channel);
201 private boolean isRegistered() {
202 return service.isRegistered(this);
206 public String getUDN() {
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);
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);
227 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
228 if (service == null) {
231 subscription = succeeded;
232 logger.debug("{}: Subscription to service {} {}", host, service, succeeded ? "succeeded" : "failed");
236 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
237 if (variable == null || value == null || service == null || variable.isBlank()) {
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);
248 stateMap.put(variable, value);
251 case "A_ARG_TYPE_LastChange":
252 parseEventValues(value);
255 handler.valueReceived(PROGRAM_TITLE, new StringType(value));
258 handler.valueReceived(CHANNEL_NAME, new StringType(value));
260 case "ExternalSource":
261 handler.valueReceived(SOURCE_NAME, new StringType(value));
264 handler.valueReceived(CHANNEL, new DecimalType(value));
267 handler.valueReceived(SOURCE_ID, new DecimalType(value));
270 handler.valueReceived(BROWSER_URL, new StringType(value));
275 protected Map<String, String> updateResourceState(String actionId) {
276 return updateResourceState(actionId, Map.of());
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"));
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);
290 onValueReceived(a, val, SERVICE_MAIN_AGENT);
296 public boolean isOk(Map<String, String> result) {
297 return result.getOrDefault("Result", "Error").equals("OK");
301 * Searches sources for source, or ID, and sets TV input to that value
303 private boolean setSourceName(Command command) {
304 String tmpSource = command.toString();
305 if (sources.isEmpty()) {
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));
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()));
322 private boolean stopBrowser() {
323 Map<String, String> result = updateResourceState("StopBrowser");
324 logResult(result.getOrDefault("Result", "Unable to Stop Browser"));
328 private void logResult(String ok) {
329 if ("OK".equals(ok)) {
330 logger.debug("{}: Command successfully executed", host);
332 logger.warn("{}: Command execution failed, result='{}'", host, ok);
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");
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", ""))))
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);
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()
362 * @param xml{@link String} which contains XML content.
364 public void parseEventValues(String xml) {
365 Utils.loadXMLFromString(xml, host).ifPresent(a -> visitRecursively(a));
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)));
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);
382 updateResourceState("GetCurrentBrowserURL");
386 logger.debug("{}: TV has Powered Off", host);
387 handler.setOffline();
392 case "ExternalSource":
395 logger.trace("{}: Processing {}:{}", host, el.getNodeName(), el.getTextContent());
396 onValueReceived(el.getNodeName(), el.getTextContent(), SERVICE_MAIN_AGENT);
401 visitRecursively(node);
405 public void onStatusChanged(boolean status) {
406 logger.trace("{}: onStatusChanged: status={}", host, status);
408 handler.setOffline();