]> git.basschouten.com Git - openhab-addons.git/blob
791cff683c3b20c27a29095744144611eaa005f6
[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.LinkedHashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Optional;
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.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;
41
42 /**
43  * The {@link MediaRendererService} is responsible for handling MediaRenderer
44  * commands.
45  *
46  * @author Pauli Anttila - Initial contribution
47  * @author Nick Waterton - added checkConnection(), getServiceName, refactored
48  */
49 @NonNullByDefault
50 public class MediaRendererService implements UpnpIOParticipant, SamsungTvService {
51
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,
56             COLOR_TEMPERATURE);
57     protected static final int SUBSCRIPTION_DURATION = 1800;
58     private static final List<String> ON_VALUE = List.of("true", "1");
59
60     private final UpnpIOService service;
61
62     private final String udn;
63     private String host = "";
64
65     private final SamsungTvHandler handler;
66
67     private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
68
69     private boolean started;
70     private boolean subscription;
71
72     public MediaRendererService(UpnpIOService upnpIOService, String udn, String host, SamsungTvHandler handler) {
73         this.service = upnpIOService;
74         this.udn = udn;
75         this.handler = handler;
76         this.host = host;
77         logger.debug("{}: Creating a Samsung TV MediaRenderer service: subscription={}", host, getSubscription());
78     }
79
80     private boolean getSubscription() {
81         return handler.configuration.getSubscription();
82     }
83
84     @Override
85     public String getServiceName() {
86         return SERVICE_NAME;
87     }
88
89     @Override
90     public List<String> getSupportedChannelNames(boolean refresh) {
91         if (refresh) {
92             if (subscription) {
93                 // Have to do this because old TV's don't update subscriptions properly
94                 if (handler.configuration.isWebsocketProtocol()) {
95                     return List.of();
96                 }
97             }
98             return SUPPORTED_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     }
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 VOLUME:
146                         updateResourceState("GetVolume");
147                         break;
148                     case MUTE:
149                         updateResourceState("GetMute");
150                         break;
151                     case BRIGHTNESS:
152                         updateResourceState("GetBrightness");
153                         break;
154                     case CONTRAST:
155                         updateResourceState("GetContrast");
156                         break;
157                     case SHARPNESS:
158                         updateResourceState("GetSharpness");
159                         break;
160                     case COLOR_TEMPERATURE:
161                         updateResourceState("GetColorTemperature");
162                         break;
163                     default:
164                         break;
165                 }
166             }
167             return true;
168         }
169
170         switch (channel) {
171             case VOLUME:
172                 if (command instanceof DecimalType) {
173                     result = sendCommand("SetVolume", cmdToString(command));
174                 }
175                 break;
176             case MUTE:
177                 if (command instanceof OnOffType) {
178                     result = sendCommand("SetMute", cmdToString(command));
179                 }
180                 break;
181             case BRIGHTNESS:
182                 if (command instanceof DecimalType) {
183                     result = sendCommand("SetBrightness", cmdToString(command));
184                 }
185                 break;
186             case CONTRAST:
187                 if (command instanceof DecimalType) {
188                     result = sendCommand("SetContrast", cmdToString(command));
189                 }
190                 break;
191             case SHARPNESS:
192                 if (command instanceof DecimalType) {
193                     result = sendCommand("SetSharpness", cmdToString(command));
194                 }
195                 break;
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));
200                 }
201                 break;
202             default:
203                 logger.warn("{}: Samsung TV doesn't support transmitting for channel '{}'", host, channel);
204                 return false;
205         }
206         if (!result) {
207             logger.warn("{}: media renderer: wrong command type {} channel {}", host, command, channel);
208         }
209         return result;
210     }
211
212     private boolean isRegistered() {
213         return service.isRegistered(this);
214     }
215
216     @Override
217     public String getUDN() {
218         return udn;
219     }
220
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);
226         }
227     }
228
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);
234         }
235     }
236
237     @Override
238     public void onServiceSubscribed(@Nullable String service, boolean succeeded) {
239         if (service == null) {
240             return;
241         }
242         subscription = succeeded;
243         logger.debug("{}: Subscription to service {} {}", host, service, succeeded ? "succeeded" : "failed");
244     }
245
246     @Override
247     public void onValueReceived(@Nullable String variable, @Nullable String value, @Nullable String service) {
248         if (variable == null || value == null || service == null || variable.isBlank()) {
249             return;
250         }
251
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);
256             return;
257         }
258
259         stateMap.put(variable, value);
260
261         switch (variable) {
262             case "LastChange":
263                 stateMap.remove("InstanceID");
264                 parseEventValues(value);
265                 break;
266             case "Volume":
267                 handler.valueReceived(VOLUME, new PercentType(value));
268                 break;
269             case "Mute":
270                 handler.valueReceived(MUTE,
271                         ON_VALUE.stream().anyMatch(value::equalsIgnoreCase) ? OnOffType.ON : OnOffType.OFF);
272                 break;
273             case "Brightness":
274                 handler.valueReceived(BRIGHTNESS, new PercentType(value));
275                 break;
276             case "Contrast":
277                 handler.valueReceived(CONTRAST, new PercentType(value));
278                 break;
279             case "Sharpness":
280                 handler.valueReceived(SHARPNESS, new PercentType(value));
281                 break;
282             case "ColorTemperature":
283                 handler.valueReceived(COLOR_TEMPERATURE, new DecimalType(value));
284                 break;
285         }
286     }
287
288     protected Map<String, String> updateResourceState(String actionId) {
289         return updateResourceState(actionId, Map.of());
290     }
291
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");
296         }
297         inputsMap.putAll(inputs);
298         Map<String, String> result = service.invokeAction(this, SERVICE_RENDERING_CONTROL, actionId, inputsMap);
299         if (!subscription) {
300             result.keySet().stream().forEach(a -> onValueReceived(a, result.get(a), SERVICE_RENDERING_CONTROL));
301         }
302         return result;
303     }
304
305     private boolean sendCommand(String command, String value) {
306         updateResourceState(command, Map.of(command.replace("Set", "Desired"), value));
307         if (!subscription) {
308             updateResourceState(command.replace("Set", "Get"));
309         }
310         return true;
311     }
312
313     private String cmdToString(Command command) {
314         if (command instanceof DecimalType commandAsDecimalType) {
315             return Integer.toString(commandAsDecimalType.intValue());
316         }
317         if (command instanceof OnOffType) {
318             return Boolean.toString(command.equals(OnOffType.ON));
319         }
320         return command.toString();
321     }
322
323     /**
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()
327      *
328      * @param xml{@link String} which contains XML content.
329      */
330     public void parseEventValues(String xml) {
331         Utils.loadXMLFromString(xml, host).ifPresent(a -> visitRecursively(a));
332     }
333
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)));
338     }
339
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"));
345             }
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"))) {
349                     return;
350                 }
351                 logger.trace("{}: Processing {}:{}", host, el.getNodeName(), el.getAttribute("val"));
352                 onValueReceived(el.getNodeName(), el.getAttribute("val"), SERVICE_RENDERING_CONTROL);
353             }
354         }
355         // visit child node
356         visitRecursively(node);
357     }
358
359     @Override
360     public void onStatusChanged(boolean status) {
361         logger.trace("{}: onStatusChanged: status={}", host, status);
362         if (!status) {
363             handler.setOffline();
364         }
365     }
366 }