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.Objects;
22 import java.util.Optional;
23 import java.util.stream.Collectors;
24 import java.util.stream.IntStream;
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;
44 * The {@link MainTVServerService} is responsible for handling MainTVServer
47 * @author Pauli Anttila - Initial contribution
48 * @author Nick Waterton - add checkConnection(), getServiceName, some refactoring
51 public class MainTVServerService implements UpnpIOParticipant, SamsungTvService {
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);
62 private final UpnpIOService service;
64 private final String udn;
65 private String host = "";
67 private final SamsungTvHandler handler;
69 private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
70 private Map<String, String> sources = Collections.synchronizedMap(new HashMap<>());
72 private boolean started;
73 private boolean subscription;
75 public MainTVServerService(UpnpIOService upnpIOService, String udn, String host, SamsungTvHandler handler) {
76 this.service = upnpIOService;
78 this.handler = handler;
80 logger.debug("{}: Creating a Samsung TV MainTVServer service: subscription={}", host, getSubscription());
83 private boolean getSubscription() {
84 return handler.configuration.getSubscription();
88 public String getServiceName() {
93 public List<String> getSupportedChannelNames(boolean refresh) {
96 return SUBSCRIPTION_REFRESH_CHANNELS;
98 return REFRESH_CHANNELS;
100 logger.trace("{}: getSupportedChannelNames: {}", host, SUPPORTED_CHANNELS);
101 return SUPPORTED_CHANNELS;
105 public void start() {
106 service.registerParticipant(this);
113 removeSubscription();
114 service.unregisterParticipant(this);
119 public void clearCache() {
125 public boolean isUpnp() {
130 public boolean checkConnection() {
135 public boolean handleCommand(String channel, Command command) {
136 logger.trace("{}: Received channel: {}, command: {}", host, channel, command);
137 boolean result = false;
139 if (!checkConnection()) {
143 if (command == RefreshType.REFRESH) {
144 if (isRegistered()) {
147 updateResourceState("GetCurrentMainTVChannel");
151 updateResourceState("GetCurrentExternalSource");
155 updateResourceState("GetCurrentContentRecognition");
158 updateResourceState("GetCurrentBrowserURL");
169 if (command instanceof DecimalType) {
170 command = new StringType(command.toString());
173 if (command instanceof StringType) {
174 result = setSourceName(command);
175 updateResourceState("GetCurrentExternalSource");
179 if (command instanceof StringType) {
180 result = setBrowserUrl(command);
184 if (command instanceof OnOffType) {
185 // stop browser if command is On or Off
186 result = stopBrowser();
188 onValueReceived("BrowserURL", "", SERVICE_MAIN_AGENT);
193 logger.warn("{}: Samsung TV doesn't support send for channel '{}'", host, channel);
197 logger.warn("{}: main tvservice: command error {} channel {}", host, command, channel);
202 private boolean isRegistered() {
203 return service.isRegistered(this);
207 public String getUDN() {
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);
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);
228 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
229 if (service == null) {
232 subscription = succeeded;
233 logger.debug("{}: Subscription to service {} {}", host, service, succeeded ? "succeeded" : "failed");
237 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
238 if (variable == null || value == null || service == null || variable.isBlank()) {
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);
249 stateMap.put(variable, value);
252 case "A_ARG_TYPE_LastChange":
253 parseEventValues(value);
256 handler.valueReceived(PROGRAM_TITLE, new StringType(value));
259 handler.valueReceived(CHANNEL_NAME, new StringType(value));
261 case "ExternalSource":
262 handler.valueReceived(SOURCE_NAME, new StringType(value));
265 handler.valueReceived(CHANNEL, new DecimalType(value));
268 handler.valueReceived(SOURCE_ID, new DecimalType(value));
271 handler.valueReceived(BROWSER_URL, new StringType(value));
276 protected Map<String, String> updateResourceState(String actionId) {
277 return updateResourceState(actionId, Map.of());
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")));
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);
291 onValueReceived(a, val, SERVICE_MAIN_AGENT);
297 public boolean isOk(Map<String, String> result) {
298 return result.getOrDefault("Result", "Error").equals("OK");
302 * Searches sources for source, or ID, and sets TV input to that value
304 private boolean setSourceName(Command command) {
305 String tmpSource = command.toString();
306 if (sources.isEmpty()) {
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));
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()));
323 private boolean stopBrowser() {
324 Map<String, String> result = updateResourceState("StopBrowser");
325 logResult(result.getOrDefault("Result", "Unable to Stop Browser"));
329 private void logResult(String ok) {
330 if ("OK".equals(ok)) {
331 logger.debug("{}: Command successfully executed", host);
333 logger.warn("{}: Command execution failed, result='{}'", host, ok);
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"));
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)))
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));
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()
364 * @param xml{@link String} which contains XML content.
366 public void parseEventValues(String xml) {
367 Utils.loadXMLFromString(xml, host).ifPresent(a -> visitRecursively(a));
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)));
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);
384 updateResourceState("GetCurrentBrowserURL");
388 logger.debug("{}: TV has Powered Off", host);
389 handler.setOffline();
394 case "ExternalSource":
397 logger.trace("{}: Processing {}:{}", host, el.getNodeName(), el.getTextContent());
398 onValueReceived(el.getNodeName(), el.getTextContent(), SERVICE_MAIN_AGENT);
403 visitRecursively(node);
407 public void onStatusChanged(boolean status) {
408 logger.trace("{}: onStatusChanged: status={}", host, status);
410 handler.setOffline();