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.FritzAhaSetColorTemperatureCallback;
39 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingModeCallback;
40 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetHeatingTemperatureCallback;
41 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetLevelPercentageCallback;
42 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaSetSwitchCallback;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * This class handles requests to a FRITZ!OS web interface for interfacing with AVM home automation devices. It manages
50 * authentication and wraps commands.
52 * @author Robert Bausdorf, Christian Brauers - Initial contribution
53 * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet
55 * @author Christoph Weitkamp - Added support for groups
56 * @author Ulrich Mertin - Added support for HAN-FUN blinds
57 * @author Christoph Sommer - Added support for color temperature
60 public class FritzAhaWebInterface {
62 private static final String WEBSERVICE_PATH = "login_sid.lua";
64 * RegEx Pattern to grab the session ID from a login XML response
66 private static final Pattern SID_PATTERN = Pattern.compile("<SID>([a-fA-F0-9]*)</SID>");
68 * RegEx Pattern to grab the challenge from a login XML response
70 private static final Pattern CHALLENGE_PATTERN = Pattern.compile("<Challenge>(\\w*)</Challenge>");
72 * RegEx Pattern to grab the access privilege for home automation functions from a login XML response
74 private static final Pattern ACCESS_PATTERN = Pattern.compile("<Name>HomeAuto</Name>\\s*?<Access>([0-9])</Access>");
76 private final Logger logger = LoggerFactory.getLogger(FritzAhaWebInterface.class);
78 * Configuration of the bridge from {@link AVMFritzBaseBridgeHandler}
80 private final AVMFritzBoxConfiguration config;
82 * Bridge thing handler for updating thing status
84 private final AVMFritzBaseBridgeHandler handler;
86 * Shared instance of HTTP client for asynchronous calls
88 private final HttpClient httpClient;
92 private @Nullable String sid;
95 * This method authenticates with the FRITZ!OS Web Interface and updates the session ID accordingly
97 public void authenticate() {
99 String localPassword = config.password;
100 if (localPassword == null || localPassword.trim().isEmpty()) {
101 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
102 "Please configure the password.");
105 String loginXml = syncGet(getURL(WEBSERVICE_PATH, addSID("")));
106 if (loginXml == null) {
107 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
108 "FRITZ!Box does not respond.");
111 Matcher sidmatch = SID_PATTERN.matcher(loginXml);
112 if (!sidmatch.find()) {
113 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
114 "FRITZ!Box does not respond with SID.");
117 String localSid = sidmatch.group(1);
118 Matcher accmatch = ACCESS_PATTERN.matcher(loginXml);
119 if (accmatch.find()) {
120 if ("2".equals(accmatch.group(1))) {
122 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
126 Matcher challengematch = CHALLENGE_PATTERN.matcher(loginXml);
127 if (!challengematch.find()) {
128 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
129 "FRITZ!Box does not respond with challenge for authentication.");
132 String challenge = challengematch.group(1);
133 String response = createResponse(challenge);
134 String localUser = config.user;
135 loginXml = syncGet(getURL(WEBSERVICE_PATH,
136 (localUser == null || localUser.isEmpty() ? "" : ("username=" + localUser + "&")) + "response="
138 if (loginXml == null) {
139 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
140 "FRITZ!Box does not respond.");
143 sidmatch = SID_PATTERN.matcher(loginXml);
144 if (!sidmatch.find()) {
145 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
146 "FRITZ!Box does not respond with SID.");
149 localSid = sidmatch.group(1);
150 accmatch = ACCESS_PATTERN.matcher(loginXml);
151 if (accmatch.find()) {
152 if ("2".equals(accmatch.group(1))) {
154 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
158 handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User "
159 + (localUser == null ? "" : localUser) + " has no access to FRITZ!Box home automation functions.");
164 * Checks the authentication status of the web interface
168 public boolean isAuthenticated() {
173 * Creates the proper response to a given challenge based on the password stored
175 * @param challenge Challenge string as returned by the FRITZ!OS login script
176 * @return Response to the challenge
178 protected String createResponse(String challenge) {
179 String response = challenge.concat("-");
180 String handshake = response.concat(config.password);
183 md5 = MessageDigest.getInstance("MD5");
184 } catch (NoSuchAlgorithmException e) {
185 logger.error("This version of Java does not support MD5 hashing");
188 byte[] handshakeHash = md5.digest(handshake.getBytes(StandardCharsets.UTF_16LE));
189 for (byte handshakeByte : handshakeHash) {
190 response = response.concat(String.format("%02x", handshakeByte));
196 * Constructor to set up interface
198 * @param config Bridge configuration
200 public FritzAhaWebInterface(AVMFritzBoxConfiguration config, AVMFritzBaseBridgeHandler handler,
201 HttpClient httpClient) {
202 this.config = config;
203 this.handler = handler;
204 this.httpClient = httpClient;
206 logger.debug("Starting with SID {}", sid);
210 * Constructs an URL from the stored information and a specified path
212 * @param path Path to include in URL
215 public String getURL(String path) {
216 return config.protocol + "://" + config.ipAddress + (config.port == null ? "" : ":" + config.port) + "/" + path;
220 * Constructs an URL from the stored information, a specified path and a specified argument string
222 * @param path Path to include in URL
223 * @param args String of arguments, in standard HTTP format ({@code arg1=value1&arg2=value2&...})
226 public String getURL(String path, String args) {
227 return getURL(args.isEmpty() ? path : path + "?" + args);
230 public String addSID(String path) {
234 return (path.isEmpty() ? "" : path + "&") + "sid=" + sid;
239 * Sends a HTTP GET request using the synchronous client
241 * @param path Path of the requested resource
244 public @Nullable String syncGet(String path) {
246 ContentResponse contentResponse = httpClient.newRequest(path)
247 .timeout(config.syncTimeout, TimeUnit.MILLISECONDS).method(HttpMethod.GET).send();
248 String content = contentResponse.getContentAsString();
249 logger.debug("GET response complete: {}", content);
251 } catch (ExecutionException | TimeoutException e) {
252 logger.debug("GET response failed: {}", e.getLocalizedMessage(), e);
254 } catch (InterruptedException e) {
255 logger.debug("GET response interrupted: {}", e.getLocalizedMessage(), e);
256 Thread.currentThread().interrupt();
262 * Sends a HTTP GET request using the asynchronous client
264 * @param path Path of the requested resource
265 * @param args Arguments for the request
266 * @param callback Callback to handle the response with
268 public FritzAhaContentExchange asyncGet(String path, String args, FritzAhaCallback callback) {
269 if (!isAuthenticated()) {
272 FritzAhaContentExchange getExchange = new FritzAhaContentExchange(callback);
273 httpClient.newRequest(getURL(path, addSID(args))).method(HttpMethod.GET).onResponseSuccess(getExchange)
274 .onResponseFailure(getExchange).send(getExchange);
278 public FritzAhaContentExchange asyncGet(FritzAhaCallback callback) {
279 return asyncGet(callback.getPath(), callback.getArgs(), callback);
283 * Sends a HTTP POST request using the asynchronous client
285 * @param path Path of the requested resource
286 * @param args Arguments for the request
287 * @param callback Callback to handle the response with
289 public FritzAhaContentExchange asyncPost(String path, String args, FritzAhaCallback callback) {
290 if (!isAuthenticated()) {
293 FritzAhaContentExchange postExchange = new FritzAhaContentExchange(callback);
294 httpClient.newRequest(getURL(path)).timeout(config.asyncTimeout, TimeUnit.MILLISECONDS).method(HttpMethod.POST)
295 .onResponseSuccess(postExchange).onResponseFailure(postExchange)
296 .content(new StringContentProvider(addSID(args), StandardCharsets.UTF_8)).send(postExchange);
300 public FritzAhaContentExchange applyTemplate(String ain) {
301 FritzAhaApplyTemplateCallback callback = new FritzAhaApplyTemplateCallback(this, ain);
302 return asyncGet(callback);
305 public FritzAhaContentExchange setSwitch(String ain, boolean switchOn) {
306 FritzAhaSetSwitchCallback callback = new FritzAhaSetSwitchCallback(this, ain, switchOn);
307 return asyncGet(callback);
310 public FritzAhaContentExchange setSetTemp(String ain, BigDecimal temperature) {
311 FritzAhaSetHeatingTemperatureCallback callback = new FritzAhaSetHeatingTemperatureCallback(this, ain,
313 return asyncGet(callback);
316 public FritzAhaContentExchange setBoostMode(String ain, long endTime) {
317 return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.BOOST_COMMAND, endTime);
320 public FritzAhaContentExchange setWindowOpenMode(String ain, long endTime) {
321 return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.WINDOW_OPEN_COMMAND, endTime);
324 private FritzAhaContentExchange setHeatingMode(String ain, String command, long endTime) {
325 FritzAhaSetHeatingModeCallback callback = new FritzAhaSetHeatingModeCallback(this, ain, command, endTime);
326 return asyncGet(callback);
329 public FritzAhaContentExchange setLevelPercentage(String ain, BigDecimal levelPercentage) {
330 FritzAhaSetLevelPercentageCallback callback = new FritzAhaSetLevelPercentageCallback(this, ain,
332 return asyncGet(callback);
335 public FritzAhaContentExchange setMappedHueAndSaturation(String ain, int hue, int saturation, int duration) {
336 FritzAhaSetColorCallback callback = new FritzAhaSetColorCallback(this, ain, hue, saturation, duration);
337 return asyncGet(callback);
340 public FritzAhaContentExchange setUnmappedHueAndSaturation(String ain, int hue, int saturation, int duration) {
341 FritzAhaSetColorCallback callback = new FritzAhaSetColorCallback(this, ain, hue, saturation, duration, false);
342 return asyncGet(callback);
345 public FritzAhaContentExchange setColorTemperature(String ain, int temperature, int duration) {
346 FritzAhaSetColorTemperatureCallback callback = new FritzAhaSetColorTemperatureCallback(this, ain, temperature,
348 return asyncGet(callback);
351 public FritzAhaContentExchange setBlind(String ain, BlindCommand command) {
352 FritzAhaSetBlindTargetCallback callback = new FritzAhaSetBlindTargetCallback(this, ain, command);
353 return asyncGet(callback);