]> git.basschouten.com Git - openhab-addons.git/blob
b695105c2be0d129ec6fc2af584c889de6c09216
[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         enigma2HttpClient = new Enigma2HttpClient(requestTimeout);
86         factory = DocumentBuilderFactory.newInstance();
87         // see https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
88         try {
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         } catch (ParserConfigurationException e) {
95             logger.warn("Failed setting parser features against XXE attacks!", e);
96         }
97         if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(password)) {
98             this.host = "http://" + user + ":" + password + "@" + host;
99         } else {
100             this.host = "http://" + host;
101         }
102     }
103
104     public boolean refresh() {
105         boolean wasOnline = online;
106         refreshPower();
107         if (!wasOnline && online) {
108             // Only refresh all services if the box changed from offline to online and power is on
109             // because it is a performance intensive action.
110             refreshAllServices();
111         }
112         refreshChannel();
113         refreshEpg();
114         refreshVolume();
115         refreshAnswer();
116         return online;
117     }
118
119     public void refreshPower() {
120         Optional<Document> document = transmitWithResult(PATH_POWER);
121         if (document.isPresent()) {
122             online = true;
123             processPowerResult(document.get());
124         } else {
125             online = false;
126             power = false;
127         }
128         initialized = true;
129     }
130
131     public void refreshAllServices() {
132         if (power || channels.isEmpty()) {
133             transmitWithResult(PATH_ALL_SERVICES).ifPresent(this::processAllServicesResult);
134         }
135     }
136
137     public void refreshChannel() {
138         if (power) {
139             transmitWithResult(PATH_CHANNEL).ifPresent(this::processChannelResult);
140         }
141     }
142
143     public void refreshAnswer() {
144         if (asking) {
145             transmitWithResult(PATH_ANSWER).ifPresent(this::processAnswerResult);
146         }
147     }
148
149     public void refreshVolume() {
150         if (power) {
151             transmitWithResult(PATH_VOLUME).ifPresent(this::processVolumeResult);
152         }
153     }
154
155     public void refreshEpg() {
156         if (power) {
157             Optional.ofNullable(channels.get(channel))
158                     .flatMap(name -> transmitWithResult(PATH_EPG + UrlEncoded.encodeString(name)))
159                     .ifPresent(this::processEpgResult);
160         }
161     }
162
163     private Optional<Document> transmitWithResult(String path) {
164         try {
165             Optional<String> xml = transmit(path);
166             if (xml.isPresent()) {
167                 DocumentBuilder builder = factory.newDocumentBuilder();
168                 return Optional.ofNullable(builder.parse(new InputSource(new StringReader(xml.get()))));
169             }
170             return Optional.empty();
171         } catch (IOException | SAXException | ParserConfigurationException | IllegalArgumentException e) {
172             if (online || !initialized) {
173                 logger.debug("Error on transmit {}{}.", host, path, e);
174             }
175             return Optional.empty();
176         }
177     }
178
179     private Optional<String> transmit(String path) {
180         String url = host + path;
181         try {
182             logger.debug("Transmitting {}", url);
183             String result = getEnigma2HttpClient().get(url);
184             logger.debug("Transmitting result is {}", result);
185             return Optional.ofNullable(result);
186         } catch (IOException | IllegalArgumentException e) {
187             if (online || !initialized) {
188                 logger.debug("Error on transmit {}.", url, e);
189             }
190             return Optional.empty();
191         }
192     }
193
194     public void setMute(boolean mute) {
195         refreshVolume();
196         if (this.mute != mute) {
197             transmitWithResult(PATH_TOGGLE_MUTE).ifPresent(this::processVolumeResult);
198         }
199     }
200
201     public void setPower(boolean power) {
202         refreshPower();
203         if (this.power != power) {
204             transmitWithResult(PATH_TOGGLE_POWER).ifPresent(this::processPowerResult);
205         }
206     }
207
208     public void setVolume(int volume) {
209         transmitWithResult(PATH_SET_VOLUME + volume).ifPresent(this::processVolumeResult);
210     }
211
212     public void setChannel(String name) {
213         if (channels.containsKey(name)) {
214             String id = channels.get(name);
215             transmitWithResult(PATH_ZAP + UrlEncoded.encodeString(id)).ifPresent(document -> channel = name);
216         } else {
217             logger.warn("Channel {} not found.", name);
218         }
219     }
220
221     public void sendRcCommand(int key) {
222         transmit(PATH_REMOTE_CONTROL + key);
223     }
224
225     public void sendError(int timeout, String text) {
226         sendMessage(TYPE_ERROR, timeout, text);
227     }
228
229     public void sendWarning(int timeout, String text) {
230         sendMessage(TYPE_WARNING, timeout, text);
231     }
232
233     public void sendInfo(int timeout, String text) {
234         sendMessage(TYPE_INFO, timeout, text);
235     }
236
237     public void sendQuestion(int timeout, String text) {
238         asking = true;
239         sendMessage(TYPE_QUESTION, timeout, text);
240     }
241
242     private void sendMessage(int type, int timeout, String text) {
243         transmit(PATH_MESSAGE + type + "&timeout=" + timeout + "&text=" + UrlEncoded.encodeString(text));
244     }
245
246     private void processPowerResult(Document document) {
247         power = !getBoolean(document, "e2instandby");
248         if (!power) {
249             title = "";
250             description = "";
251             channel = "";
252         }
253     }
254
255     private void processChannelResult(Document document) {
256         channel = getString(document, "e2servicename");
257         // Add channel-Reference-ID if not known
258         if (!channels.containsKey(channel)) {
259             channels.put(channel, getString(document, "e2servicereference"));
260         }
261     }
262
263     private void processAnswerResult(Document document) {
264         if (asking) {
265             boolean state = getBoolean(document, "e2state");
266             if (state) {
267                 String[] text = getString(document, "e2statetext").split(" ");
268                 answer = text[text.length - 1].replace("!", "");
269                 asking = false;
270                 lastAnswerTime = LocalDateTime.now();
271             }
272         }
273     }
274
275     private void processVolumeResult(Document document) {
276         volume = getInt(document, "e2current");
277         mute = getBoolean(document, "e2ismuted");
278     }
279
280     private void processEpgResult(Document document) {
281         title = getString(document, "e2eventtitle");
282         description = getString(document, "e2eventdescription");
283     }
284
285     private void processAllServicesResult(Document document) {
286         NodeList bouquetList = document.getElementsByTagName("e2bouquet");
287         channels.clear();
288         for (int i = 0; i < bouquetList.getLength(); i++) {
289             Element bouquet = (Element) bouquetList.item(i);
290             NodeList serviceList = bouquet.getElementsByTagName("e2service");
291             for (int j = 0; j < serviceList.getLength(); j++) {
292                 Element service = (Element) serviceList.item(j);
293                 String id = service.getElementsByTagName("e2servicereference").item(0).getTextContent();
294                 String name = service.getElementsByTagName("e2servicename").item(0).getTextContent();
295                 channels.put(name, id);
296             }
297         }
298     }
299
300     private String getString(Document document, String elementId) {
301         return Optional.ofNullable(document.getElementsByTagName(elementId)).map(nodeList -> nodeList.item(0))
302                 .map(Node::getTextContent).map(String::trim).orElse("");
303     }
304
305     private boolean getBoolean(Document document, String elementId) {
306         return Boolean.parseBoolean(getString(document, elementId));
307     }
308
309     private int getInt(Document document, String elementId) {
310         try {
311             return Integer.parseInt(getString(document, elementId));
312         } catch (NumberFormatException e) {
313             return 0;
314         }
315     }
316
317     public int getVolume() {
318         return volume;
319     }
320
321     public boolean isMute() {
322         return mute;
323     }
324
325     public boolean isPower() {
326         return power;
327     }
328
329     public LocalDateTime getLastAnswerTime() {
330         return lastAnswerTime;
331     }
332
333     public String getChannel() {
334         return channel;
335     }
336
337     public String getTitle() {
338         return title;
339     }
340
341     public String getDescription() {
342         return description;
343     }
344
345     public String getAnswer() {
346         return answer;
347     }
348
349     public Collection<String> getChannels() {
350         return channels.keySet();
351     }
352
353     /**
354      * Getter for Test-Injection
355      *
356      * @return HttpGet.
357      */
358     Enigma2HttpClient getEnigma2HttpClient() {
359         return enigma2HttpClient;
360     }
361 }