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