]> git.basschouten.com Git - openhab-addons.git/blob
833e87eac1b9b92a48d96be19819e673f06bbe67
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.enigma2.internal;
14
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.time.LocalDateTime;
18 import java.util.Collection;
19 import java.util.Map;
20 import java.util.Optional;
21 import java.util.concurrent.ConcurrentHashMap;
22
23 import javax.xml.parsers.DocumentBuilder;
24 import javax.xml.parsers.DocumentBuilderFactory;
25 import javax.xml.parsers.ParserConfigurationException;
26
27 import org.apache.commons.lang.StringUtils;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.util.UrlEncoded;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import org.w3c.dom.Document;
34 import org.w3c.dom.Element;
35 import org.w3c.dom.Node;
36 import org.w3c.dom.NodeList;
37 import org.xml.sax.InputSource;
38 import org.xml.sax.SAXException;
39
40 /**
41  * The {@link Enigma2Client} class is responsible for communicating with the Enigma2 device.
42  *
43  * @see <a href=
44  *      "https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/OpenWebif-API-documentation">OpenWebif-API-documentation</a>
45  *
46  * @author Guido Dolfen - Initial contribution
47  */
48 @NonNullByDefault
49 public class Enigma2Client {
50     private final Logger logger = LoggerFactory.getLogger(Enigma2Client.class);
51
52     static final String PATH_REMOTE_CONTROL = "/web/remotecontrol?command=";
53     static final String PATH_POWER = "/web/powerstate";
54     static final String PATH_VOLUME = "/web/vol";
55     static final String PATH_SET_VOLUME = "/web/vol?set=set";
56     static final String PATH_TOGGLE_MUTE = "/web/vol?set=mute";
57     static final String PATH_TOGGLE_POWER = "/web/powerstate?newstate=0";
58     static final String PATH_MESSAGE = "/web/message?type=";
59     static final String PATH_ALL_SERVICES = "/web/getallservices";
60     static final String PATH_ZAP = "/web/zap?sRef=";
61     static final String PATH_CHANNEL = "/web/subservices";
62     static final String PATH_EPG = "/web/epgservicenow?sRef=";
63     static final String PATH_ANSWER = "/web/messageanswer?getanswer=now";
64     static final int TYPE_QUESTION = 0;
65     static final int TYPE_INFO = 1;
66     static final int TYPE_WARNING = 2;
67     static final int TYPE_ERROR = 3;
68     private final Map<String, String> channels = new ConcurrentHashMap<>();
69     private final String host;
70     private boolean power;
71     private String channel = "";
72     private String title = "";
73     private String description = "";
74     private String answer = "";
75     private int volume = 0;
76     private boolean mute;
77     private boolean online;
78     private boolean initialized;
79     private boolean asking;
80     private LocalDateTime lastAnswerTime = LocalDateTime.of(2020, 1, 1, 0, 0); // Date in the past
81     private final Enigma2HttpClient enigma2HttpClient;
82     private final DocumentBuilderFactory factory;
83
84     public Enigma2Client(String host, @Nullable String user, @Nullable String password, int requestTimeout)
85             throws ParserConfigurationException {
86         enigma2HttpClient = new Enigma2HttpClient(requestTimeout);
87         factory = DocumentBuilderFactory.newInstance();
88         // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
89         factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
90         factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
91         factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
92         factory.setXIncludeAware(false);
93         factory.setExpandEntityReferences(false);
94         if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(password)) {
95             this.host = "http://" + user + ":" + password + "@" + host;
96         } else {
97             this.host = "http://" + host;
98         }
99     }
100
101     public boolean refresh() {
102         boolean wasOnline = online;
103         refreshPower();
104         if (!wasOnline && online) {
105             // Only refresh all services if the box changed from offline to online and power is on
106             // because it is a performance intensive action.
107             refreshAllServices();
108         }
109         refreshChannel();
110         refreshEpg();
111         refreshVolume();
112         refreshAnswer();
113         return online;
114     }
115
116     public void refreshPower() {
117         Optional<Document> document = transmitWithResult(PATH_POWER);
118         if (document.isPresent()) {
119             online = true;
120             processPowerResult(document.get());
121         } else {
122             online = false;
123             power = false;
124         }
125         initialized = true;
126     }
127
128     public void refreshAllServices() {
129         if (power || channels.isEmpty()) {
130             transmitWithResult(PATH_ALL_SERVICES).ifPresent(this::processAllServicesResult);
131         }
132     }
133
134     public void refreshChannel() {
135         if (power) {
136             transmitWithResult(PATH_CHANNEL).ifPresent(this::processChannelResult);
137         }
138     }
139
140     public void refreshAnswer() {
141         if (asking) {
142             transmitWithResult(PATH_ANSWER).ifPresent(this::processAnswerResult);
143         }
144     }
145
146     public void refreshVolume() {
147         if (power) {
148             transmitWithResult(PATH_VOLUME).ifPresent(this::processVolumeResult);
149         }
150     }
151
152     public void refreshEpg() {
153         if (power) {
154             Optional.ofNullable(channels.get(channel))
155                     .flatMap(name -> transmitWithResult(PATH_EPG + UrlEncoded.encodeString(name)))
156                     .ifPresent(this::processEpgResult);
157         }
158     }
159
160     private Optional<Document> transmitWithResult(String path) {
161         try {
162             Optional<String> xml = transmit(path);
163             if (xml.isPresent()) {
164                 DocumentBuilder builder = factory.newDocumentBuilder();
165                 return Optional.ofNullable(builder.parse(new InputSource(new StringReader(xml.get()))));
166             }
167             return Optional.empty();
168         } catch (IOException | SAXException | ParserConfigurationException | IllegalArgumentException e) {
169             if (online || !initialized) {
170                 logger.debug("Error on transmit {}{}.", host, path, e);
171             }
172             return Optional.empty();
173         }
174     }
175
176     private Optional<String> transmit(String path) {
177         String url = host + path;
178         try {
179             logger.debug("Transmitting {}", url);
180             String result = getEnigma2HttpClient().get(url);
181             logger.debug("Transmitting result is {}", result);
182             return Optional.ofNullable(result);
183         } catch (IOException | IllegalArgumentException e) {
184             if (online || !initialized) {
185                 logger.debug("Error on transmit {}.", url, e);
186             }
187             return Optional.empty();
188         }
189     }
190
191     public void setMute(boolean mute) {
192         refreshVolume();
193         if (this.mute != mute) {
194             transmitWithResult(PATH_TOGGLE_MUTE).ifPresent(this::processVolumeResult);
195         }
196     }
197
198     public void setPower(boolean power) {
199         refreshPower();
200         if (this.power != power) {
201             transmitWithResult(PATH_TOGGLE_POWER).ifPresent(this::processPowerResult);
202         }
203     }
204
205     public void setVolume(int volume) {
206         transmitWithResult(PATH_SET_VOLUME + volume).ifPresent(this::processVolumeResult);
207     }
208
209     public void setChannel(String name) {
210         if (channels.containsKey(name)) {
211             String id = channels.get(name);
212             transmitWithResult(PATH_ZAP + UrlEncoded.encodeString(id)).ifPresent(document -> channel = name);
213         } else {
214             logger.warn("Channel {} not found.", name);
215         }
216     }
217
218     public void sendRcCommand(int key) {
219         transmit(PATH_REMOTE_CONTROL + key);
220     }
221
222     public void sendError(int timeout, String text) {
223         sendMessage(TYPE_ERROR, timeout, text);
224     }
225
226     public void sendWarning(int timeout, String text) {
227         sendMessage(TYPE_WARNING, timeout, text);
228     }
229
230     public void sendInfo(int timeout, String text) {
231         sendMessage(TYPE_INFO, timeout, text);
232     }
233
234     public void sendQuestion(int timeout, String text) {
235         asking = true;
236         sendMessage(TYPE_QUESTION, timeout, text);
237     }
238
239     private void sendMessage(int type, int timeout, String text) {
240         transmit(PATH_MESSAGE + type + "&timeout=" + timeout + "&text=" + UrlEncoded.encodeString(text));
241     }
242
243     private void processPowerResult(Document document) {
244         power = !getBoolean(document, "e2instandby");
245         if (!power) {
246             title = "";
247             description = "";
248             channel = "";
249         }
250     }
251
252     private void processChannelResult(Document document) {
253         channel = getString(document, "e2servicename");
254         // Add channel-Reference-ID if not known
255         if (!channels.containsKey(channel)) {
256             channels.put(channel, getString(document, "e2servicereference"));
257         }
258     }
259
260     private void processAnswerResult(Document document) {
261         if (asking) {
262             boolean state = getBoolean(document, "e2state");
263             if (state) {
264                 String[] text = getString(document, "e2statetext").split(" ");
265                 answer = text[text.length - 1].replace("!", "");
266                 asking = false;
267                 lastAnswerTime = LocalDateTime.now();
268             }
269         }
270     }
271
272     private void processVolumeResult(Document document) {
273         volume = getInt(document, "e2current");
274         mute = getBoolean(document, "e2ismuted");
275     }
276
277     private void processEpgResult(Document document) {
278         title = getString(document, "e2eventtitle");
279         description = getString(document, "e2eventdescription");
280     }
281
282     private void processAllServicesResult(Document document) {
283         NodeList bouquetList = document.getElementsByTagName("e2bouquet");
284         channels.clear();
285         for (int i = 0; i < bouquetList.getLength(); i++) {
286             Element bouquet = (Element) bouquetList.item(i);
287             NodeList serviceList = bouquet.getElementsByTagName("e2service");
288             for (int j = 0; j < serviceList.getLength(); j++) {
289                 Element service = (Element) serviceList.item(j);
290                 String id = service.getElementsByTagName("e2servicereference").item(0).getTextContent();
291                 String name = service.getElementsByTagName("e2servicename").item(0).getTextContent();
292                 channels.put(name, id);
293             }
294         }
295     }
296
297     private String getString(Document document, String elementId) {
298         return Optional.ofNullable(document.getElementsByTagName(elementId)).map(nodeList -> nodeList.item(0))
299                 .map(Node::getTextContent).map(String::trim).orElse("");
300     }
301
302     private boolean getBoolean(Document document, String elementId) {
303         return Boolean.parseBoolean(getString(document, elementId));
304     }
305
306     private int getInt(Document document, String elementId) {
307         try {
308             return Integer.parseInt(getString(document, elementId));
309         } catch (NumberFormatException e) {
310             return 0;
311         }
312     }
313
314     public int getVolume() {
315         return volume;
316     }
317
318     public boolean isMute() {
319         return mute;
320     }
321
322     public boolean isPower() {
323         return power;
324     }
325
326     public LocalDateTime getLastAnswerTime() {
327         return lastAnswerTime;
328     }
329
330     public String getChannel() {
331         return channel;
332     }
333
334     public String getTitle() {
335         return title;
336     }
337
338     public String getDescription() {
339         return description;
340     }
341
342     public String getAnswer() {
343         return answer;
344     }
345
346     public Collection<String> getChannels() {
347         return channels.keySet();
348     }
349
350     /**
351      * Getter for Test-Injection
352      *
353      * @return HttpGet.
354      */
355     Enigma2HttpClient getEnigma2HttpClient() {
356         return enigma2HttpClient;
357     }
358 }