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.LinkedHashMap;
20 import java.util.List;
22 import java.util.Optional;
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.PercentType;
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 MediaRendererService} is responsible for handling MediaRenderer
46 * @author Pauli Anttila - Initial contribution
47 * @author Nick Waterton - added checkConnection(), getServiceName, refactored
50 public class MediaRendererService implements UpnpIOParticipant, SamsungTvService {
52 private final Logger logger = LoggerFactory.getLogger(MediaRendererService.class);
53 public static final String SERVICE_NAME = "MediaRenderer";
54 private static final String SERVICE_RENDERING_CONTROL = "RenderingControl";
55 private static final List<String> SUPPORTED_CHANNELS = List.of(VOLUME, MUTE, BRIGHTNESS, CONTRAST, SHARPNESS,
57 protected static final int SUBSCRIPTION_DURATION = 1800;
58 private static final List<String> ON_VALUE = List.of("true", "1");
60 private final UpnpIOService service;
62 private final String udn;
63 private String host = "";
65 private final SamsungTvHandler handler;
67 private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
69 private boolean started;
70 private boolean subscription;
72 public MediaRendererService(UpnpIOService upnpIOService, String udn, String host, SamsungTvHandler handler) {
73 this.service = upnpIOService;
75 this.handler = handler;
77 logger.debug("{}: Creating a Samsung TV MediaRenderer service: subscription={}", host, getSubscription());
80 private boolean getSubscription() {
81 return handler.configuration.getSubscription();
85 public String getServiceName() {
90 public List<String> getSupportedChannelNames(boolean refresh) {
93 // Have to do this because old TV's don't update subscriptions properly
94 if (handler.configuration.isWebsocketProtocol()) {
98 return SUPPORTED_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() {
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("GetVolume");
149 updateResourceState("GetMute");
152 updateResourceState("GetBrightness");
155 updateResourceState("GetContrast");
158 updateResourceState("GetSharpness");
160 case COLOR_TEMPERATURE:
161 updateResourceState("GetColorTemperature");
172 if (command instanceof DecimalType) {
173 result = sendCommand("SetVolume", cmdToString(command));
177 if (command instanceof OnOffType) {
178 result = sendCommand("SetMute", cmdToString(command));
182 if (command instanceof DecimalType) {
183 result = sendCommand("SetBrightness", cmdToString(command));
187 if (command instanceof DecimalType) {
188 result = sendCommand("SetContrast", cmdToString(command));
192 if (command instanceof DecimalType) {
193 result = sendCommand("SetSharpness", cmdToString(command));
196 case COLOR_TEMPERATURE:
197 if (command instanceof DecimalType commandAsDecimalType) {
198 int newValue = Math.max(0, Math.min(commandAsDecimalType.intValue(), 4));
199 result = sendCommand("SetColorTemperature", Integer.toString(newValue));
203 logger.warn("{}: Samsung TV doesn't support transmitting for channel '{}'", host, channel);
207 logger.warn("{}: media renderer: wrong command type {} channel {}", host, command, channel);
212 private boolean isRegistered() {
213 return service.isRegistered(this);
217 public String getUDN() {
221 private void addSubscription() {
222 // Set up GENA Subscriptions
223 if (isRegistered() && getSubscription()) {
224 logger.debug("{}: Subscribing to service {}...", host, SERVICE_RENDERING_CONTROL);
225 service.addSubscription(this, SERVICE_RENDERING_CONTROL, SUBSCRIPTION_DURATION);
229 private void removeSubscription() {
230 // Remove GENA Subscriptions
231 if (isRegistered() && subscription) {
232 logger.debug("{}: Unsubscribing from service {}...", host, SERVICE_RENDERING_CONTROL);
233 service.removeSubscription(this, SERVICE_RENDERING_CONTROL);
238 public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
239 if (service == null) {
242 subscription = succeeded;
243 logger.debug("{}: Subscription to service {} {}", host, service, succeeded ? "succeeded" : "failed");
247 public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
248 if (variable == null || value == null || service == null || variable.isBlank()) {
252 variable = variable.replace("Current", "");
253 String oldValue = stateMap.getOrDefault(variable, "None");
254 if (value.equals(oldValue)) {
255 logger.trace("{}: Value '{}' for {} hasn't changed, ignoring update", host, value, variable);
259 stateMap.put(variable, value);
263 stateMap.remove("InstanceID");
264 parseEventValues(value);
267 handler.valueReceived(VOLUME, new PercentType(value));
270 handler.valueReceived(MUTE,
271 ON_VALUE.stream().anyMatch(value::equalsIgnoreCase) ? OnOffType.ON : OnOffType.OFF);
274 handler.valueReceived(BRIGHTNESS, new PercentType(value));
277 handler.valueReceived(CONTRAST, new PercentType(value));
280 handler.valueReceived(SHARPNESS, new PercentType(value));
282 case "ColorTemperature":
283 handler.valueReceived(COLOR_TEMPERATURE, new DecimalType(value));
288 protected Map<String, String> updateResourceState(String actionId) {
289 return updateResourceState(actionId, Map.of());
292 protected synchronized Map<String, String> updateResourceState(String actionId, Map<String, String> inputs) {
293 Map<String, String> inputsMap = new LinkedHashMap<String, String>(Map.of("InstanceID", "0"));
294 if (Utils.isSoundChannel(actionId)) {
295 inputsMap.put("Channel", "Master");
297 inputsMap.putAll(inputs);
298 Map<String, String> result = service.invokeAction(this, SERVICE_RENDERING_CONTROL, actionId, inputsMap);
300 result.keySet().stream().forEach(a -> onValueReceived(a, result.get(a), SERVICE_RENDERING_CONTROL));
305 private boolean sendCommand(String command, String value) {
306 updateResourceState(command, Map.of(command.replace("Set", "Desired"), value));
308 updateResourceState(command.replace("Set", "Get"));
313 private String cmdToString(Command command) {
314 if (command instanceof DecimalType commandAsDecimalType) {
315 return Integer.toString(commandAsDecimalType.intValue());
317 if (command instanceof OnOffType) {
318 return Boolean.toString(command.equals(OnOffType.ON));
320 return command.toString();
324 * Parse Subscription Event from {@link String} which contains XML content.
325 * Parses all child Nodes recursively.
326 * If valid channel update is found, call onValueReceived()
328 * @param xml{@link String} which contains XML content.
330 public void parseEventValues(String xml) {
331 Utils.loadXMLFromString(xml, host).ifPresent(a -> visitRecursively(a));
334 public void visitRecursively(Node node) {
335 // get all child nodes, NodeList doesn't have a stream, so do this
336 Optional.ofNullable(node.getChildNodes()).ifPresent(nList -> IntStream.range(0, nList.getLength())
337 .mapToObj(i -> (Node) nList.item(i)).forEach(childNode -> parseNode(childNode)));
340 public void parseNode(Node node) {
341 if (node.getNodeType() == Node.ELEMENT_NODE) {
342 Element el = (Element) node;
343 if ("InstanceID".equals(el.getNodeName())) {
344 stateMap.put(el.getNodeName(), el.getAttribute("val"));
346 if (SUPPORTED_CHANNELS.stream().filter(a -> "0".equals(stateMap.get("InstanceID")))
347 .anyMatch(el.getNodeName()::equalsIgnoreCase)) {
348 if (Utils.isSoundChannel(el.getNodeName()) && !"Master".equals(el.getAttribute("channel"))) {
351 logger.trace("{}: Processing {}:{}", host, el.getNodeName(), el.getAttribute("val"));
352 onValueReceived(el.getNodeName(), el.getAttribute("val"), SERVICE_RENDERING_CONTROL);
356 visitRecursively(node);
360 public void onStatusChanged(boolean status) {
361 logger.trace("{}: onStatusChanged: status={}", host, status);
363 handler.setOffline();