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.avmfritz.internal.hardware;
15 import static org.eclipse.jetty.http.HttpMethod.*;
17 import java.math.BigDecimal;
18 import java.nio.charset.StandardCharsets;
19 import java.security.MessageDigest;
20 import java.security.NoSuchAlgorithmException;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.openhab.binding.avmfritz.internal.config.AVMFritzBoxConfiguration;
33 import org.openhab.binding.avmfritz.internal.handler.AVMFritzBaseBridgeHandler;
34 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaApplyTemplateCallback;
35 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaCallback;
36 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingModeCallback;
37 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingTemperatureCallback;
38 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetSwitchCallback;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
45 * This class handles requests to a FRITZ!OS web interface for interfacing with AVM home automation devices. It manages
46 * authentication and wraps commands.
48 * @author Robert Bausdorf, Christian Brauers - Initial contribution
49 * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet
51 * @author Christoph Weitkamp - Added support for groups
54 public class FritzAhaWebInterface {
56 private static final String WEBSERVICE_PATH = "login_sid.lua";
58 * RegEx Pattern to grab the session ID from a login XML response
60 private static final Pattern SID_PATTERN = Pattern.compile("<SID>([a-fA-F0-9]*)</SID>");
62 * RegEx Pattern to grab the challenge from a login XML response
64 private static final Pattern CHALLENGE_PATTERN = Pattern.compile("<Challenge>(\\w*)</Challenge>");
66 * RegEx Pattern to grab the access privilege for home automation functions from a login XML response
68 private static final Pattern ACCESS_PATTERN = Pattern.compile("<Name>HomeAuto</Name>\\s*?<Access>([0-9])</Access>");
70 private final Logger logger = LoggerFactory.getLogger(FritzAhaWebInterface.class);
72 * Configuration of the bridge from {@link AVMFritzBaseBridgeHandler}
74 private final AVMFritzBoxConfiguration config;
76 * Bridge thing handler for updating thing status
78 private final AVMFritzBaseBridgeHandler handler;
80 * Shared instance of HTTP client for asynchronous calls
82 private final HttpClient httpClient;
86 private @Nullable String sid;
89 * This method authenticates with the FRITZ!OS Web Interface and updates the session ID accordingly
91 public void authenticate() {
93 String localPassword = config.password;
94 if (localPassword == null || localPassword.trim().isEmpty()) {
95 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
96 "Please configure the password.");
99 String loginXml = syncGet(getURL(WEBSERVICE_PATH, addSID("")));
100 if (loginXml == null) {
101 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
102 "FRITZ!Box does not respond.");
105 Matcher sidmatch = SID_PATTERN.matcher(loginXml);
106 if (!sidmatch.find()) {
107 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
108 "FRITZ!Box does not respond with SID.");
111 String localSid = sidmatch.group(1);
112 Matcher accmatch = ACCESS_PATTERN.matcher(loginXml);
113 if (accmatch.find()) {
114 if ("2".equals(accmatch.group(1))) {
116 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
120 Matcher challengematch = CHALLENGE_PATTERN.matcher(loginXml);
121 if (!challengematch.find()) {
122 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
123 "FRITZ!Box does not respond with challenge for authentication.");
126 String challenge = challengematch.group(1);
127 String response = createResponse(challenge);
128 String localUser = config.user;
129 loginXml = syncGet(getURL(WEBSERVICE_PATH,
130 (localUser == null || localUser.isEmpty() ? "" : ("username=" + localUser + "&")) + "response="
132 if (loginXml == null) {
133 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
134 "FRITZ!Box does not respond.");
137 sidmatch = SID_PATTERN.matcher(loginXml);
138 if (!sidmatch.find()) {
139 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
140 "FRITZ!Box does not respond with SID.");
143 localSid = sidmatch.group(1);
144 accmatch = ACCESS_PATTERN.matcher(loginXml);
145 if (accmatch.find()) {
146 if ("2".equals(accmatch.group(1))) {
148 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
152 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User "
153 + (localUser == null ? "" : localUser) + " has no access to FRITZ!Box home automation functions.");
158 * Checks the authentication status of the web interface
162 public boolean isAuthenticated() {
167 * Creates the proper response to a given challenge based on the password stored
169 * @param challenge Challenge string as returned by the FRITZ!OS login script
170 * @return Response to the challenge
172 protected String createResponse(String challenge) {
173 String response = challenge.concat("-");
174 String handshake = response.concat(config.password);
177 md5 = MessageDigest.getInstance("MD5");
178 } catch (NoSuchAlgorithmException e) {
179 logger.error("This version of Java does not support MD5 hashing");
182 byte[] handshakeHash = md5.digest(handshake.getBytes(StandardCharsets.UTF_16LE));
183 for (byte handshakeByte : handshakeHash) {
184 response = response.concat(String.format("%02x", handshakeByte));
190 * Constructor to set up interface
192 * @param config Bridge configuration
194 public FritzAhaWebInterface(AVMFritzBoxConfiguration config, AVMFritzBaseBridgeHandler handler,
195 HttpClient httpClient) {
196 this.config = config;
197 this.handler = handler;
198 this.httpClient = httpClient;
200 logger.debug("Starting with SID {}", sid);
204 * Constructs an URL from the stored information and a specified path
206 * @param path Path to include in URL
209 public String getURL(String path) {
210 return config.protocol + "://" + config.ipAddress + (config.port == null ? "" : ":" + config.port) + "/" + path;
214 * Constructs an URL from the stored information, a specified path and a specified argument string
216 * @param path Path to include in URL
217 * @param args String of arguments, in standard HTTP format (arg1=value1&arg2=value2&...)
220 public String getURL(String path, String args) {
221 return getURL(args.isEmpty() ? path : path + "?" + args);
224 public String addSID(String path) {
228 return (path.isEmpty() ? "" : path + "&") + "sid=" + sid;
233 * Sends a HTTP GET request using the synchronous client
235 * @param path Path of the requested resource
238 public @Nullable String syncGet(String url) {
240 ContentResponse contentResponse = httpClient.newRequest(url)
241 .timeout(config.syncTimeout, TimeUnit.MILLISECONDS).method(GET).send();
242 String content = contentResponse.getContentAsString();
243 logger.debug("GET response complete: {}", content);
245 } catch (ExecutionException | TimeoutException e) {
246 logger.debug("response failed: {}", e.getLocalizedMessage(), e);
248 } catch (InterruptedException e) {
249 logger.debug("response interrupted: {}", e.getLocalizedMessage(), e);
250 Thread.currentThread().interrupt();
256 * Sends a HTTP GET request using the asynchronous client
258 * @param path Path of the requested resource
259 * @param args Arguments for the request
260 * @param callback Callback to handle the response with
262 public FritzAhaContentExchange asyncGet(String path, String args, FritzAhaCallback callback) {
263 if (!isAuthenticated()) {
266 FritzAhaContentExchange getExchange = new FritzAhaContentExchange(callback);
267 httpClient.newRequest(getURL(path, addSID(args))).method(GET).onResponseSuccess(getExchange)
268 .onResponseFailure(getExchange).send(getExchange);
272 public FritzAhaContentExchange asyncGet(FritzAhaCallback callback) {
273 return asyncGet(callback.getPath(), callback.getArgs(), callback);
277 * Sends a HTTP POST request using the asynchronous client
279 * @param path Path of the requested resource
280 * @param args Arguments for the request
281 * @param callback Callback to handle the response with
283 public FritzAhaContentExchange asyncPost(String path, String args, FritzAhaCallback callback) {
284 if (!isAuthenticated()) {
287 FritzAhaContentExchange postExchange = new FritzAhaContentExchange(callback);
288 httpClient.newRequest(getURL(path)).timeout(config.asyncTimeout, TimeUnit.MILLISECONDS).method(POST)
289 .onResponseSuccess(postExchange).onResponseFailure(postExchange)
290 .content(new StringContentProvider(addSID(args), StandardCharsets.UTF_8)).send(postExchange);
294 public FritzAhaContentExchange applyTemplate(String ain) {
295 FritzAhaApplyTemplateCallback callback = new FritzAhaApplyTemplateCallback(this, ain);
296 return asyncGet(callback);
299 public FritzAhaContentExchange setSwitch(String ain, boolean switchOn) {
300 FritzAhaSetSwitchCallback callback = new FritzAhaSetSwitchCallback(this, ain, switchOn);
301 return asyncGet(callback);
304 public FritzAhaContentExchange setSetTemp(String ain, BigDecimal temperature) {
305 FritzAhaSetHeatingTemperatureCallback callback = new FritzAhaSetHeatingTemperatureCallback(this, ain,
307 return asyncGet(callback);
310 public FritzAhaContentExchange setBoostMode(String ain, long endTime) {
311 return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.BOOST_COMMAND, endTime);
314 public FritzAhaContentExchange setWindowOpenMode(String ain, long endTime) {
315 return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.WINDOW_OPEN_COMMAND, endTime);
318 private FritzAhaContentExchange setHeatingMode(String ain, String command, long endTime) {
319 FritzAhaSetHeatingModeCallback callback = new FritzAhaSetHeatingModeCallback(this, ain, command, endTime);
320 return asyncGet(callback);