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