]> git.basschouten.com Git - openhab-addons.git/blob
73de6bf7a3aacaf5a63fc9ffd86397d98aba48e3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.avmfritz.internal.hardware;
14
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;
24
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;
46
47 /**
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.
50  *
51  * @author Robert Bausdorf, Christian Brauers - Initial contribution
52  * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet
53  *         DECT
54  * @author Christoph Weitkamp - Added support for groups
55  * @author Ulrich Mertin - Added support for HAN-FUN blinds
56  */
57 @NonNullByDefault
58 public class FritzAhaWebInterface {
59
60     private static final String WEBSERVICE_PATH = "login_sid.lua";
61     /**
62      * RegEx Pattern to grab the session ID from a login XML response
63      */
64     private static final Pattern SID_PATTERN = Pattern.compile("<SID>([a-fA-F0-9]*)</SID>");
65     /**
66      * RegEx Pattern to grab the challenge from a login XML response
67      */
68     private static final Pattern CHALLENGE_PATTERN = Pattern.compile("<Challenge>(\\w*)</Challenge>");
69     /**
70      * RegEx Pattern to grab the access privilege for home automation functions from a login XML response
71      */
72     private static final Pattern ACCESS_PATTERN = Pattern.compile("<Name>HomeAuto</Name>\\s*?<Access>([0-9])</Access>");
73
74     private final Logger logger = LoggerFactory.getLogger(FritzAhaWebInterface.class);
75     /**
76      * Configuration of the bridge from {@link AVMFritzBaseBridgeHandler}
77      */
78     private final AVMFritzBoxConfiguration config;
79     /**
80      * Bridge thing handler for updating thing status
81      */
82     private final AVMFritzBaseBridgeHandler handler;
83     /**
84      * Shared instance of HTTP client for asynchronous calls
85      */
86     private final HttpClient httpClient;
87     /**
88      * Current session ID
89      */
90     private @Nullable String sid;
91
92     /**
93      * This method authenticates with the FRITZ!OS Web Interface and updates the session ID accordingly
94      */
95     public void authenticate() {
96         sid = null;
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.");
101             return;
102         }
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.");
107             return;
108         }
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.");
113             return;
114         }
115         String localSid = sidmatch.group(1);
116         Matcher accmatch = ACCESS_PATTERN.matcher(loginXml);
117         if (accmatch.find()) {
118             if ("2".equals(accmatch.group(1))) {
119                 sid = localSid;
120                 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
121                 return;
122             }
123         }
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.");
128             return;
129         }
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="
135                         + response));
136         if (loginXml == null) {
137             handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
138                     "FRITZ!Box does not respond.");
139             return;
140         }
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.");
145             return;
146         }
147         localSid = sidmatch.group(1);
148         accmatch = ACCESS_PATTERN.matcher(loginXml);
149         if (accmatch.find()) {
150             if ("2".equals(accmatch.group(1))) {
151                 sid = localSid;
152                 handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
153                 return;
154             }
155         }
156         handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User "
157                 + (localUser == null ? "" : localUser) + " has no access to FRITZ!Box home automation functions.");
158         return;
159     }
160
161     /**
162      * Checks the authentication status of the web interface
163      *
164      * @return
165      */
166     public boolean isAuthenticated() {
167         return sid != null;
168     }
169
170     /**
171      * Creates the proper response to a given challenge based on the password stored
172      *
173      * @param challenge Challenge string as returned by the FRITZ!OS login script
174      * @return Response to the challenge
175      */
176     protected String createResponse(String challenge) {
177         String response = challenge.concat("-");
178         String handshake = response.concat(config.password);
179         MessageDigest md5;
180         try {
181             md5 = MessageDigest.getInstance("MD5");
182         } catch (NoSuchAlgorithmException e) {
183             logger.error("This version of Java does not support MD5 hashing");
184             return "";
185         }
186         byte[] handshakeHash = md5.digest(handshake.getBytes(StandardCharsets.UTF_16LE));
187         for (byte handshakeByte : handshakeHash) {
188             response = response.concat(String.format("%02x", handshakeByte));
189         }
190         return response;
191     }
192
193     /**
194      * Constructor to set up interface
195      *
196      * @param config Bridge configuration
197      */
198     public FritzAhaWebInterface(AVMFritzBoxConfiguration config, AVMFritzBaseBridgeHandler handler,
199             HttpClient httpClient) {
200         this.config = config;
201         this.handler = handler;
202         this.httpClient = httpClient;
203         authenticate();
204         logger.debug("Starting with SID {}", sid);
205     }
206
207     /**
208      * Constructs an URL from the stored information and a specified path
209      *
210      * @param path Path to include in URL
211      * @return URL
212      */
213     public String getURL(String path) {
214         return config.protocol + "://" + config.ipAddress + (config.port == null ? "" : ":" + config.port) + "/" + path;
215     }
216
217     /**
218      * Constructs an URL from the stored information, a specified path and a specified argument string
219      *
220      * @param path Path to include in URL
221      * @param args String of arguments, in standard HTTP format (arg1=value1&arg2=value2&...)
222      * @return URL
223      */
224     public String getURL(String path, String args) {
225         return getURL(args.isEmpty() ? path : path + "?" + args);
226     }
227
228     public String addSID(String path) {
229         if (sid == null) {
230             return path;
231         } else {
232             return (path.isEmpty() ? "" : path + "&") + "sid=" + sid;
233         }
234     }
235
236     /**
237      * Sends a HTTP GET request using the synchronous client
238      *
239      * @param path Path of the requested resource
240      * @return response
241      */
242     public @Nullable String syncGet(String path) {
243         try {
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);
248             return content;
249         } catch (ExecutionException | TimeoutException e) {
250             logger.debug("GET response failed: {}", e.getLocalizedMessage(), e);
251             return null;
252         } catch (InterruptedException e) {
253             logger.debug("GET response interrupted: {}", e.getLocalizedMessage(), e);
254             Thread.currentThread().interrupt();
255             return null;
256         }
257     }
258
259     /**
260      * Sends a HTTP GET request using the asynchronous client
261      *
262      * @param path Path of the requested resource
263      * @param args Arguments for the request
264      * @param callback Callback to handle the response with
265      */
266     public FritzAhaContentExchange asyncGet(String path, String args, FritzAhaCallback callback) {
267         if (!isAuthenticated()) {
268             authenticate();
269         }
270         FritzAhaContentExchange getExchange = new FritzAhaContentExchange(callback);
271         httpClient.newRequest(getURL(path, addSID(args))).method(HttpMethod.GET).onResponseSuccess(getExchange)
272                 .onResponseFailure(getExchange).send(getExchange);
273         return getExchange;
274     }
275
276     public FritzAhaContentExchange asyncGet(FritzAhaCallback callback) {
277         return asyncGet(callback.getPath(), callback.getArgs(), callback);
278     }
279
280     /**
281      * Sends a HTTP POST request using the asynchronous client
282      *
283      * @param path Path of the requested resource
284      * @param args Arguments for the request
285      * @param callback Callback to handle the response with
286      */
287     public FritzAhaContentExchange asyncPost(String path, String args, FritzAhaCallback callback) {
288         if (!isAuthenticated()) {
289             authenticate();
290         }
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);
295         return postExchange;
296     }
297
298     public FritzAhaContentExchange applyTemplate(String ain) {
299         FritzAhaApplyTemplateCallback callback = new FritzAhaApplyTemplateCallback(this, ain);
300         return asyncGet(callback);
301     }
302
303     public FritzAhaContentExchange setSwitch(String ain, boolean switchOn) {
304         FritzAhaSetSwitchCallback callback = new FritzAhaSetSwitchCallback(this, ain, switchOn);
305         return asyncGet(callback);
306     }
307
308     public FritzAhaContentExchange setSetTemp(String ain, BigDecimal temperature) {
309         FritzAhaSetHeatingTemperatureCallback callback = new FritzAhaSetHeatingTemperatureCallback(this, ain,
310                 temperature);
311         return asyncGet(callback);
312     }
313
314     public FritzAhaContentExchange setBoostMode(String ain, long endTime) {
315         return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.BOOST_COMMAND, endTime);
316     }
317
318     public FritzAhaContentExchange setWindowOpenMode(String ain, long endTime) {
319         return setHeatingMode(ain, FritzAhaSetHeatingModeCallback.WINDOW_OPEN_COMMAND, endTime);
320     }
321
322     private FritzAhaContentExchange setHeatingMode(String ain, String command, long endTime) {
323         FritzAhaSetHeatingModeCallback callback = new FritzAhaSetHeatingModeCallback(this, ain, command, endTime);
324         return asyncGet(callback);
325     }
326
327     public FritzAhaContentExchange setLevelPercentage(String ain, BigDecimal levelPercentage) {
328         FritzAhaSetLevelPercentageCallback callback = new FritzAhaSetLevelPercentageCallback(this, ain,
329                 levelPercentage);
330         return asyncGet(callback);
331     }
332
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);
336     }
337
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);
341     }
342
343     public FritzAhaContentExchange setBlind(String ain, BlindCommand command) {
344         FritzAhaSetBlindTargetCallback callback = new FritzAhaSetBlindTargetCallback(this, ain, command);
345         return asyncGet(callback);
346     }
347 }