]> git.basschouten.com Git - openhab-addons.git/blob
7cf852c3c63ba60f3425eb87222bc528d9d65922
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.argoclima.internal.device.api;
14
15 import java.net.InetAddress;
16 import java.net.URL;
17 import java.net.URLEncoder;
18 import java.nio.charset.StandardCharsets;
19 import java.text.MessageFormat;
20 import java.util.Objects;
21 import java.util.Optional;
22 import java.util.SortedMap;
23 import java.util.function.Consumer;
24 import java.util.regex.Pattern;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.argoclima.internal.ArgoClimaTranslationProvider;
29 import org.openhab.binding.argoclima.internal.configuration.ArgoClimaConfigurationRemote;
30 import org.openhab.binding.argoclima.internal.device.api.DeviceStatus.DeviceProperties;
31 import org.openhab.binding.argoclima.internal.exception.ArgoApiCommunicationException;
32 import org.openhab.binding.argoclima.internal.exception.ArgoApiProtocolViolationException;
33 import org.openhab.core.i18n.TimeZoneProvider;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38  * Argo protocol implementation for a REMOTE connection to the device
39  * <p>
40  * The HVAC device MUST be communicating with actual Argo servers for this method work.
41  * This means the device is either directly connected to the Internet (w/o traffic intercept), or there's an
42  * intercepting Stub server already running in a PASS-THROUGH mode (sniffing the messages but passing through to the
43  * actual vendor's servers)
44  *
45  * <p>
46  * Use of this mode is actually NOT recommended for advanced users as cleartext device and Wi-Fi passwords are sent to
47  * Argo servers through unencrypted HTTP connection (sic!). If the Argo UI access is desired (ex. for FW update or IR
48  * remote-like experience), consider using this mode only on a dedicated Wi-Fi network (and possibly through VPN)
49  *
50  * @author Mateusz Bronk - Initial contribution
51  *
52  */
53 @NonNullByDefault
54 public class ArgoClimaRemoteDevice extends ArgoClimaDeviceApiBase {
55     private final Logger logger = LoggerFactory.getLogger(getClass());
56
57     private final InetAddress oemServerHostname;
58     private final int oemServerPort;
59     private final String usernameUrlEncoded;
60     private final String passwordMD5Hash;
61     private static final Pattern REMOTE_API_RESPONSE_EXPECTED = Pattern.compile(
62             "^[\\\\{][|](?<commands>[^|]+)[|](?<localIP>[^|]+)[|](?<lastSeen>[^|]+)[|][\\\\}]\\s*$",
63             Pattern.CASE_INSENSITIVE); // Capture group names are used in code!
64
65     /**
66      * C-tor
67      *
68      * @param config The Thing configuration
69      * @param client The common HTTP client used for issuing requests to the remote server
70      * @param timeZoneProvider System-wide TZ provider, for parsing/displaying local dates
71      * @param i18nProvider Framework's translation provider
72      * @param oemServerHostname The address of the remote (vendor's) server
73      * @param oemServerPort The port of remote (vendor's) server
74      * @param username The username used for authenticating to the remote server (will be URL-encoded before send)
75      * @param passwordMD5 A MD5 hash of the password used for authenticating to the remote server (custom Basic-like
76      *            auth)
77      * @param onDevicePropertiesUpdate Callback to invoke when device properties get refreshed
78      */
79     public ArgoClimaRemoteDevice(ArgoClimaConfigurationRemote config, HttpClient client,
80             TimeZoneProvider timeZoneProvider, ArgoClimaTranslationProvider i18nProvider, InetAddress oemServerHostname,
81             int oemServerPort, String username, String passwordMD5,
82             Consumer<SortedMap<String, String>> onDevicePropertiesUpdate) {
83         super(config, client, timeZoneProvider, i18nProvider, onDevicePropertiesUpdate, "REMOTE_API");
84         this.oemServerHostname = oemServerHostname;
85         this.oemServerPort = oemServerPort;
86         this.usernameUrlEncoded = Objects.requireNonNull(URLEncoder.encode(username, StandardCharsets.UTF_8));
87         this.passwordMD5Hash = passwordMD5;
88     }
89
90     @Override
91     public final ReachabilityStatus isReachable() {
92         try {
93             var status = extractDeviceStatusFromResponse(pollForCurrentStatusFromDeviceSync(getDeviceStateQueryUrl()));
94             try {
95                 this.deviceStatus.fromDeviceString(status.getCommandString());
96             } catch (ArgoApiProtocolViolationException e) {
97                 throw new ArgoApiCommunicationException("Unrecognized API response",
98                         "thing-status.cause.argoclima.exception.unrecognized-response", i18nProvider, e);
99             }
100             this.updateDevicePropertiesFromDeviceResponse(status.getProperties(), this.deviceStatus);
101             status.throwIfStatusIsStale();
102             return new ReachabilityStatus(true, "");
103         } catch (ArgoApiCommunicationException e) {
104             logger.debug("Device not reachable: {}", e.getMessage());
105             return new ReachabilityStatus(false,
106                     Objects.requireNonNull(MessageFormat.format(
107                             "Failed to communicate with Argo HVAC remote device at [http://{0}:{1,number,#}{2}]. {3}",
108                             this.getDeviceStateQueryUrl().getHost(),
109                             this.getDeviceStateQueryUrl().getPort() != -1 ? this.getDeviceStateQueryUrl().getPort()
110                                     : this.getDeviceStateQueryUrl().getDefaultPort(),
111                             this.getDeviceStateQueryUrl().getPath(), e.getMessage())));
112         }
113     }
114
115     @Override
116     protected URL getDeviceStateQueryUrl() {
117         // Hard-coded values are part of ARGO protocol
118         return newUrl(Objects.requireNonNull(this.oemServerHostname.getHostName()), this.oemServerPort, "/UI/UI.php",
119                 String.format("CM=UI_TC&USN=%s&PSW=%s&HMI=&UPD=0", this.usernameUrlEncoded, this.passwordMD5Hash));
120     }
121
122     @Override
123     protected URL getDeviceStateUpdateUrl() {
124         // Hard-coded values are part of ARGO protocol
125         return newUrl(Objects.requireNonNull(this.oemServerHostname.getHostName()), this.oemServerPort, "/UI/UI.php",
126                 String.format("CM=UI_TC&USN=%s&PSW=%s&HMI=%s&UPD=1", this.usernameUrlEncoded, this.passwordMD5Hash,
127                         this.deviceStatus.getDeviceCommandStatus()));
128     }
129
130     @Override
131     protected DeviceStatus extractDeviceStatusFromResponse(String apiResponse) throws ArgoApiCommunicationException {
132         if (apiResponse.isBlank()) {
133             throw new ArgoApiCommunicationException("The remote API response was empty. Check username and password",
134                     "thing-status.cause.argoclima.empty-remote-response", i18nProvider);
135         }
136
137         var matcher = REMOTE_API_RESPONSE_EXPECTED.matcher(apiResponse);
138         if (!matcher.matches()) {
139             throw new ArgoApiCommunicationException("The remote API response [%s] was not recognized",
140                     "thing-status.cause.argoclima.unrecognized-remote-response", i18nProvider, apiResponse);
141         }
142
143         // Group names must match regex above
144         var properties = new DeviceProperties(Objects.requireNonNull(matcher.group("localIP")),
145                 Objects.requireNonNull(matcher.group("lastSeen")), Optional.of(
146                         getWebUiUrl(Objects.requireNonNull(this.oemServerHostname.getHostName()), this.oemServerPort)));
147
148         return new DeviceStatus(Objects.requireNonNull(matcher.group("commands")), properties, i18nProvider);
149     }
150
151     /**
152      * Return the full URL to the Vendor's web application
153      *
154      * @param hostName The OEM server host
155      * @param port The OEM server port
156      * @return Full URL to the UI webapp
157      */
158     public static URL getWebUiUrl(String hostName, int port) {
159         return newUrl(hostName, port, "/UI/WEBAPP/webapp.php", "");
160     }
161 }