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