2 * Copyright (c) 2010-2023 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 java.math.BigDecimal;
16 import java.nio.charset.StandardCharsets;
17 import java.security.MessageDigest;
18 import java.security.NoSuchAlgorithmException;
19 import java.util.concurrent.ExecutionException;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.TimeoutException;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpMethod;
31 import org.openhab.binding.avmfritz.internal.config.AVMFritzBoxConfiguration;
32 import org.openhab.binding.avmfritz.internal.handler.AVMFritzBaseBridgeHandler;
33 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaApplyTemplateCallback;
34 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaCallback;
35 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetBlindTargetCallback;
36 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetBlindTargetCallback.BlindCommand;
37 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetColorCallback;
38 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingModeCallback;
39 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingTemperatureCallback;
40 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetLevelPercentageCallback;
41 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetSwitchCallback;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * This class handles requests to a FRITZ!OS web interface for interfacing with AVM home automation devices. It manages
49 * authentication and wraps commands.
51 * @author Robert Bausdorf, Christian Brauers - Initial contribution
52 * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet
54 * @author Christoph Weitkamp - Added support for groups
55 * @author Ulrich Mertin - Added support for HAN-FUN blinds
58 public class FritzAhaWebInterface {
60 private static final String WEBSERVICE_PATH = "login_sid.lua";
62 * RegEx Pattern to grab the session ID from a login XML response
64 private static final Pattern SID_PATTERN = Pattern.compile("<SID>([a-fA-F0-9]*)</SID>");
66 * RegEx Pattern to grab the challenge from a login XML response
68 private static final Pattern CHALLENGE_PATTERN = Pattern.compile("<Challenge>(\\w*)</Challenge>");
70 * RegEx Pattern to grab the access privilege for home automation functions from a login XML response
72 private static final Pattern ACCESS_PATTERN = Pattern.compile("<Name>HomeAuto</Name>\\s*?<Access>([0-9])</Access>");
74 private final Logger logger = LoggerFactory.getLogger(FritzAhaWebInterface.class);
76 * Configuration of the bridge from {@link AVMFritzBaseBridgeHandler}
78 private final AVMFritzBoxConfiguration config;
80 * Bridge thing handler for updating thing status
82 private final AVMFritzBaseBridgeHandler handler;
84 * Shared instance of HTTP client for asynchronous calls
86 private final HttpClient httpClient;
90 private @Nullable String sid;
93 * This method authenticates with the FRITZ!OS Web Interface and updates the session ID accordingly
95 public void authenticate() {
97 String localPassword = config.password;
98 if (localPassword == null || localPassword.trim().isEmpty()) {
99 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
100 "Please configure the password.");
103 String loginXml = syncGet(getURL(WEBSERVICE_PATH, addSID("")));
104 if (loginXml == null) {
105 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
106 "FRITZ!Box does not respond.");
109 Matcher sidmatch = SID_PATTERN.matcher(loginXml);
110 if (!sidmatch.find()) {
111 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
112 "FRITZ!Box does not respond with SID.");
115 String localSid = sidmatch.group(1);
116 Matcher accmatch = ACCESS_PATTERN.matcher(loginXml);
117 if (accmatch.find()) {
118 if ("2".equals(accmatch.group(1))) {
120 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
124 Matcher challengematch = CHALLENGE_PATTERN.matcher(loginXml);
125 if (!challengematch.find()) {
126 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
127 "FRITZ!Box does not respond with challenge for authentication.");
130 String challenge = challengematch.group(1);
131 String response = createResponse(challenge);
132 String localUser = config.user;
133 loginXml = syncGet(getURL(WEBSERVICE_PATH,
134 (localUser == null || localUser.isEmpty() ? "" : ("username=" + localUser + "&")) + "response="
136 if (loginXml == null) {
137 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
138 "FRITZ!Box does not respond.");
141 sidmatch = SID_PATTERN.matcher(loginXml);
142 if (!sidmatch.find()) {
143 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
144 "FRITZ!Box does not respond with SID.");
147 localSid = sidmatch.group(1);
148 accmatch = ACCESS_PATTERN.matcher(loginXml);
149 if (accmatch.find()) {
150 if ("2".equals(accmatch.group(1))) {
152 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
156 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User "
157 + (localUser == null ? "" : localUser) + " has no access to FRITZ!Box home automation functions.");
162 * Checks the authentication status of the web interface
166 public boolean isAuthenticated() {
171 * Creates the proper response to a given challenge based on the password stored
173 * @param challenge Challenge string as returned by the FRITZ!OS login script
174 * @return Response to the challenge
176 protected String createResponse(String challenge) {
177 String response = challenge.concat("-");
178 String handshake = response.concat(config.password);
181 md5 = MessageDigest.getInstance("MD5");
182 } catch (NoSuchAlgorithmException e) {
183 logger.error("This version of Java does not support MD5 hashing");
186 byte[] handshakeHash = md5.digest(handshake.getBytes(StandardCharsets.UTF_16LE));
187 for (byte handshakeByte : handshakeHash) {
188 response = response.concat(String.format("%02x", handshakeByte));
194 * Constructor to set up interface
196 * @param config Bridge configuration
198 public FritzAhaWebInterface(AVMFritzBoxConfiguration config, AVMFritzBaseBridgeHandler handler,
199 HttpClient httpClient) {
200 this.config = config;
201 this.handler = handler;
202 this.httpClient = httpClient;
204 logger.debug("Starting with SID {}", sid);
208 * Constructs an URL from the stored information and a specified path
210 * @param path Path to include in URL
213 public String getURL(String path) {
214 return config.protocol + "://" + config.ipAddress + (config.port == null ? "" : ":" + config.port) + "/" + path;
218 * Constructs an URL from the stored information, a specified path and a specified argument string
220 * @param path Path to include in URL
221 * @param args String of arguments, in standard HTTP format (arg1=value1&arg2=value2&...)
224 public String getURL(String path, String args) {
225 return getURL(args.isEmpty() ? path : path + "?" + args);
228 public String addSID(String path) {
232 return (path.isEmpty() ? "" : path + "&") + "sid=" + sid;
237 * Sends a HTTP GET request using the synchronous client
239 * @param path Path of the requested resource
242 public @Nullable String syncGet(String path) {
244 ContentResponse contentResponse = httpClient.newRequest(path)
245 .timeout(config.syncTimeout, TimeUnit.MILLISECONDS).method(HttpMethod.GET).send();
246 String content = contentResponse.getContentAsString();
247 logger.debug("GET response complete: {}", content);
249 } catch (ExecutionException | TimeoutException e) {
250 logger.debug("GET response failed: {}", e.getLocalizedMessage(), e);
252 } catch (InterruptedException e) {
253 logger.debug("GET response interrupted: {}", e.getLocalizedMessage(), e);
254 Thread.currentThread().interrupt();
260 * Sends a HTTP GET request using the asynchronous client
262 * @param path Path of the requested resource
263 * @param args Arguments for the request
264 * @param callback Callback to handle the response with
266 public FritzAhaContentExchange asyncGet(String path, String args, FritzAhaCallback callback) {
267 if (!isAuthenticated()) {
270 FritzAhaContentExchange getExchange = new FritzAhaContentExchange(callback);
271 httpClient.newRequest(getURL(path, addSID(args))).method(HttpMethod.GET).onResponseSuccess(getExchange)
272 .onResponseFailure(getExchange).send(getExchange);
276 public FritzAhaContentExchange asyncGet(FritzAhaCallback callback) {
277 return asyncGet(callback.getPath(), callback.getArgs(), callback);
281 * Sends a HTTP POST request using the asynchronous client
283 * @param path Path of the requested resource
284 * @param args Arguments for the request
285 * @param callback Callback to handle the response with
287 public FritzAhaContentExchange asyncPost(String path, String args, FritzAhaCallback callback) {
288 if (!isAuthenticated()) {
291 FritzAhaContentExchange postExchange = new FritzAhaContentExchange(callback);
292 httpClient.newRequest(getURL(path)).timeout(config.asyncTimeout, TimeUnit.MILLISECONDS).method(HttpMethod.POST)
293 .onResponseSuccess(postExchange).onResponseFailure(postExchange)
294 .content(new StringContentProvider(addSID(args), StandardCharsets.UTF_8)).send(postExchange);
298 public FritzAhaContentExchange applyTemplate(String ain) {
299 FritzAhaApplyTemplateCallback callback = new FritzAhaApplyTemplateCallback(this, ain);
300 return asyncGet(callback);
303 public FritzAhaContentExchange setSwitch(String ain, boolean switchOn) {
304 FritzAhaSetSwitchCallback callback = new FritzAhaSetSwitchCallback(this, ain, switchOn);
305 return asyncGet(callback);
308 public FritzAhaContentExchange setSetTemp(String ain, BigDecimal temperature) {
309 FritzAhaSetHeatingTemperatureCallback callback = new FritzAhaSetHeatingTemperatureCallback(this, ain,
311 return asyncGet(callback);
314 public FritzAhaContentExchange setBoostMode(String ain, long endTime) {
315 return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.BOOST_COMMAND, endTime);
318 public FritzAhaContentExchange setWindowOpenMode(String ain, long endTime) {
319 return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.WINDOW_OPEN_COMMAND, endTime);
322 private FritzAhaContentExchange setHeatingMode(String ain, String command, long endTime) {
323 FritzAhaSetHeatingModeCallback callback = new FritzAhaSetHeatingModeCallback(this, ain, command, endTime);
324 return asyncGet(callback);
327 public FritzAhaContentExchange setLevelPercentage(String ain, BigDecimal levelPercentage) {
328 FritzAhaSetLevelPercentageCallback callback = new FritzAhaSetLevelPercentageCallback(this, ain,
330 return asyncGet(callback);
333 public FritzAhaContentExchange setMappedHueAndSaturation(String ain, int hue, int saturation, int duration) {
334 FritzAhaSetColorCallback callback = new FritzAhaSetColorCallback(this, ain, hue, saturation, duration);
335 return asyncGet(callback);
338 public FritzAhaContentExchange setUnmappedHueAndSaturation(String ain, int hue, int saturation, int duration) {
339 FritzAhaSetColorCallback callback = new FritzAhaSetColorCallback(this, ain, hue, saturation, duration, false);
340 return asyncGet(callback);
343 public FritzAhaContentExchange setBlind(String ain, BlindCommand command) {
344 FritzAhaSetBlindTargetCallback callback = new FritzAhaSetBlindTargetCallback(this, ain, command);
345 return asyncGet(callback);