]> git.basschouten.com Git - openhab-addons.git/blob
ed7efa891cbdb535beae16a68f8bc0a881a043e3
[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.shelly.internal.manager;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
16 import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.TreeMap;
23 import java.util.concurrent.ExecutionException;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.http.HttpFields;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
35 import org.openhab.binding.shelly.internal.api.ShellyApiException;
36 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
37 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
38 import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
39 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
40 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
41 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.osgi.service.cm.ConfigurationAdmin;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * {@link ShellyManagerOtaPage} implements the Shelly Manager's download proxy for images (load them from bundle)
49  *
50  * @author Markus Michels - Initial contribution
51  */
52 @NonNullByDefault
53 public class ShellyManagerOtaPage extends ShellyManagerPage {
54     protected final Logger logger = LoggerFactory.getLogger(ShellyManagerOtaPage.class);
55
56     public ShellyManagerOtaPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
57             HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
58         super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
59     }
60
61     @Override
62     public ShellyMgrResponse generateContent(String path, Map<String, String[]> parameters) throws ShellyApiException {
63         if (path.contains(SHELLY_MGR_OTA_URI)) {
64             return loadFirmware(path, parameters);
65         } else {
66             return generatePage(path, parameters);
67         }
68     }
69
70     public ShellyMgrResponse generatePage(String path, Map<String, String[]> parameters) throws ShellyApiException {
71         String uid = getUrlParm(parameters, URLPARM_UID);
72         String version = getUrlParm(parameters, URLPARM_VERSION);
73         String update = getUrlParm(parameters, URLPARM_UPDATE);
74         String connection = getUrlParm(parameters, URLPARM_CONNECTION);
75         String url = getUrlParm(parameters, URLPARM_URL);
76         if (uid.isEmpty() || (version.isEmpty() && connection.isEmpty()) || !getThingHandlers().containsKey(uid)) {
77             return new ShellyMgrResponse("Invalid URL parameters: " + parameters, HttpStatus.BAD_REQUEST_400);
78         }
79
80         Map<String, String> properties = new HashMap<>();
81         String html = loadHTML(HEADER_HTML, properties);
82         ShellyManagerInterface th = getThingHandlers().get(uid);
83         if (th != null) {
84             properties = fillProperties(new HashMap<>(), uid, th);
85             ShellyThingConfiguration config = getThingConfig(th, properties);
86             ShellyDeviceProfile profile = th.getProfile();
87             String deviceType = getDeviceType(properties);
88
89             String uri = !url.isEmpty() && connection.equals(CONNECTION_TYPE_CUSTOM) ? url
90                     : getFirmwareUrl(config.deviceIp, deviceType, profile.mode, version,
91                             connection.equals(CONNECTION_TYPE_LOCAL));
92             if (connection.equalsIgnoreCase(CONNECTION_TYPE_INTERNET)) {
93                 // If target
94                 // - contains "update=xx" then use -> ?update=true for release and ?beta=true for beta
95                 // - otherwise qualify full url with ?url=xxxx
96                 if (uri.contains("update=") || uri.contains("beta=")) {
97                     url = uri;
98                 } else {
99                     url = URLPARM_URL + "=" + uri;
100                 }
101             } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_LOCAL)) {
102                 // redirect to local server -> http://<oh-ip>:<oh-port>/shelly/manager/ota?deviceType=xxx&version=xxx
103                 String modeParm = !profile.mode.isEmpty() ? "&" + URLPARM_DEVMODE + "=" + profile.mode : "";
104                 url = URLPARM_URL + "=http://" + localIp + ":" + localPort + SHELLY_MGR_OTA_URI + urlEncode(
105                         "?" + URLPARM_DEVTYPE + "=" + deviceType + modeParm + "&" + URLPARM_VERSION + "=" + version);
106             } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_CUSTOM)) {
107                 // else custom -> don't modify url
108                 uri = url;
109                 url = URLPARM_URL + "=" + uri;
110             }
111             String updateUrl = url;
112
113             properties.put(ATTRIBUTE_VERSION, version);
114             properties.put(ATTRIBUTE_FW_URL, uri);
115             properties.put(ATTRIBUTE_UPDATE_URL, "http://" + getDeviceIp(properties) + "/ota?" + updateUrl);
116             properties.put(URLPARM_CONNECTION, connection);
117
118             if ("yes".equalsIgnoreCase(update)) {
119                 // do the update
120                 th.setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade");
121                 html += loadHTML(FWUPDATE2_HTML, properties);
122
123                 new Thread(() -> { // schedule asynchronous reboot
124                     try {
125                         ShellyApiInterface api = th.getApi();
126                         ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
127                         String status = getString(result.status);
128                         logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));
129
130                         // Shelly Motion needs almost 2min for upgrade
131                         scheduleUpdate(th, uid + "_upgrade", profile.isMotion ? 110 : 30);
132                     } catch (ShellyApiException e) {
133                         // maybe the device restarts before returning the http response
134                         logger.warn("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", e.toString()));
135                     }
136                 }).start();
137             } else {
138                 String message = getMessageP("fwupdate.confirm", MCINFO);
139                 properties.put(ATTRIBUTE_MESSAGE, message);
140                 html += loadHTML(FWUPDATE1_HTML, properties);
141             }
142         }
143
144         html += loadHTML(FOOTER_HTML, properties);
145         return new ShellyMgrResponse(html, HttpStatus.OK_200);
146     }
147
148     protected ShellyMgrResponse loadFirmware(String path, Map<String, String[]> parameters) throws ShellyApiException {
149         String deviceType = getUrlParm(parameters, URLPARM_DEVTYPE);
150         String deviceMode = getUrlParm(parameters, URLPARM_DEVMODE);
151         String version = getUrlParm(parameters, URLPARM_VERSION);
152         String url = getUrlParm(parameters, URLPARM_URL);
153         logger.info("ShellyManager: {}", getMessage("fwupdate.info", deviceType, version, url));
154
155         String failure = getMessage("fwupdate.notfound", deviceType, version, url);
156         try {
157             if (url.isEmpty()) {
158                 url = getFirmwareUrl("", deviceType, deviceMode, version, true);
159                 if (url.isEmpty()) {
160                     logger.warn("ShellyManager: {}", failure);
161                     return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
162                 }
163             }
164
165             logger.debug("ShellyManager: Loading firmware from {}", url);
166             // BufferedInputStream in = new BufferedInputStream(new URL(url).openStream());
167             // byte[] buf = new byte[in.available()];
168             // in.read(buf);
169             Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(SHELLY_API_TIMEOUT_MS,
170                     TimeUnit.MILLISECONDS);
171             ContentResponse contentResponse = request.send();
172             HttpFields fields = contentResponse.getHeaders();
173             Map<String, String> headers = new TreeMap<>();
174             String etag = getString(fields.get("ETag"));
175             String ranges = getString(fields.get("accept-ranges"));
176             String modified = getString(fields.get("Last-Modified"));
177             headers.put("ETag", etag);
178             headers.put("accept-ranges", ranges);
179             headers.put("Last-Modified", modified);
180             byte[] data = contentResponse.getContent();
181             logger.info("ShellyManager: {}", getMessage("fwupdate.success", data.length, etag, modified));
182             return new ShellyMgrResponse(data, HttpStatus.OK_200, contentResponse.getMediaType(), headers);
183         } catch (ExecutionException | TimeoutException | InterruptedException | RuntimeException e) {
184             logger.info("ShellyManager: {}", failure, e);
185             return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
186
187         }
188     }
189
190     protected String getFirmwareUrl(String deviceIp, String deviceType, String mode, String version, boolean local)
191             throws ShellyApiException {
192         switch (version) {
193             case FWPROD:
194             case FWBETA:
195                 boolean prod = version.equals(FWPROD);
196                 if (!local) {
197                     // run regular device update
198                     return prod ? "update=true" : "beta=true";
199                 } else {
200                     // convert prod/beta to full url
201                     FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
202                     String url = getString(prod ? fw.url : fw.betaUrl);
203                     logger.debug("ShellyManager: Map {} release to url {}, version {}", url, prod ? fw.url : fw.betaUrl,
204                             prod ? fw.version : fw.betaVer);
205                     return url;
206                 }
207             default: // Update from firmware archive
208                 FwArchList list = getFirmwareArchiveList(deviceType);
209                 ArrayList<FwArchEntry> versions = list.versions;
210                 if (versions != null) {
211                     for (FwArchEntry e : versions) {
212                         String url = FWREPO_ARCFILE_URL + version + "/" + getString(e.file);
213                         if (getString(e.version).equalsIgnoreCase(version)) {
214                             return url;
215                         }
216                     }
217                 }
218         }
219         return "";
220     }
221 }