2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.enigma2.internal;
15 import java.io.IOException;
16 import java.io.StringReader;
17 import java.time.LocalDateTime;
18 import java.util.Collection;
20 import java.util.Optional;
21 import java.util.concurrent.ConcurrentHashMap;
23 import javax.xml.parsers.DocumentBuilder;
24 import javax.xml.parsers.DocumentBuilderFactory;
25 import javax.xml.parsers.ParserConfigurationException;
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;
41 * The {@link Enigma2Client} class is responsible for communicating with the Enigma2 device.
44 * "https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/OpenWebif-API-documentation">OpenWebif-API-documentation</a>
46 * @author Guido Dolfen - Initial contribution
49 public class Enigma2Client {
50 private final Logger logger = LoggerFactory.getLogger(Enigma2Client.class);
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;
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;
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
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);
97 if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(password)) {
98 this.host = "http://" + user + ":" + password + "@" + host;
100 this.host = "http://" + host;
104 public boolean refresh() {
105 boolean wasOnline = online;
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();
119 public void refreshPower() {
120 Optional<Document> document = transmitWithResult(PATH_POWER);
121 if (document.isPresent()) {
123 processPowerResult(document.get());
131 public void refreshAllServices() {
132 if (power || channels.isEmpty()) {
133 transmitWithResult(PATH_ALL_SERVICES).ifPresent(this::processAllServicesResult);
137 public void refreshChannel() {
139 transmitWithResult(PATH_CHANNEL).ifPresent(this::processChannelResult);
143 public void refreshAnswer() {
145 transmitWithResult(PATH_ANSWER).ifPresent(this::processAnswerResult);
149 public void refreshVolume() {
151 transmitWithResult(PATH_VOLUME).ifPresent(this::processVolumeResult);
155 public void refreshEpg() {
157 Optional.ofNullable(channels.get(channel))
158 .flatMap(name -> transmitWithResult(PATH_EPG + UrlEncoded.encodeString(name)))
159 .ifPresent(this::processEpgResult);
163 private Optional<Document> transmitWithResult(String path) {
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()))));
170 return Optional.empty();
171 } catch (IOException | SAXException | ParserConfigurationException | IllegalArgumentException e) {
172 if (online || !initialized) {
173 logger.debug("Error on transmit {}{}.", host, path, e);
175 return Optional.empty();
179 private Optional<String> transmit(String path) {
180 String url = host + path;
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);
190 return Optional.empty();
194 public void setMute(boolean mute) {
196 if (this.mute != mute) {
197 transmitWithResult(PATH_TOGGLE_MUTE).ifPresent(this::processVolumeResult);
201 public void setPower(boolean power) {
203 if (this.power != power) {
204 transmitWithResult(PATH_TOGGLE_POWER).ifPresent(this::processPowerResult);
208 public void setVolume(int volume) {
209 transmitWithResult(PATH_SET_VOLUME + volume).ifPresent(this::processVolumeResult);
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);
217 logger.warn("Channel {} not found.", name);
221 public void sendRcCommand(int key) {
222 transmit(PATH_REMOTE_CONTROL + key);
225 public void sendError(int timeout, String text) {
226 sendMessage(TYPE_ERROR, timeout, text);
229 public void sendWarning(int timeout, String text) {
230 sendMessage(TYPE_WARNING, timeout, text);
233 public void sendInfo(int timeout, String text) {
234 sendMessage(TYPE_INFO, timeout, text);
237 public void sendQuestion(int timeout, String text) {
239 sendMessage(TYPE_QUESTION, timeout, text);
242 private void sendMessage(int type, int timeout, String text) {
243 transmit(PATH_MESSAGE + type + "&timeout=" + timeout + "&text=" + UrlEncoded.encodeString(text));
246 private void processPowerResult(Document document) {
247 power = !getBoolean(document, "e2instandby");
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"));
263 private void processAnswerResult(Document document) {
265 boolean state = getBoolean(document, "e2state");
267 String[] text = getString(document, "e2statetext").split(" ");
268 answer = text[text.length - 1].replace("!", "");
270 lastAnswerTime = LocalDateTime.now();
275 private void processVolumeResult(Document document) {
276 volume = getInt(document, "e2current");
277 mute = getBoolean(document, "e2ismuted");
280 private void processEpgResult(Document document) {
281 title = getString(document, "e2eventtitle");
282 description = getString(document, "e2eventdescription");
285 private void processAllServicesResult(Document document) {
286 NodeList bouquetList = document.getElementsByTagName("e2bouquet");
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);
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("");
305 private boolean getBoolean(Document document, String elementId) {
306 return Boolean.parseBoolean(getString(document, elementId));
309 private int getInt(Document document, String elementId) {
311 return Integer.parseInt(getString(document, elementId));
312 } catch (NumberFormatException e) {
317 public int getVolume() {
321 public boolean isMute() {
325 public boolean isPower() {
329 public LocalDateTime getLastAnswerTime() {
330 return lastAnswerTime;
333 public String getChannel() {
337 public String getTitle() {
341 public String getDescription() {
345 public String getAnswer() {
349 public Collection<String> getChannels() {
350 return channels.keySet();
354 * Getter for Test-Injection
358 Enigma2HttpClient getEnigma2HttpClient() {
359 return enigma2HttpClient;