]> git.basschouten.com Git - openhab-addons.git/blob
901e546936c9b2b5bb40c6fdbb77a2ea7a22b584
[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.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.device.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.device.mode.isEmpty() ? "&" + URLPARM_DEVMODE + "=" + profile.device.mode
104                         : "";
105                 url = URLPARM_URL + "=http://" + localIp + ":" + localPort + SHELLY_MGR_OTA_URI + urlEncode(
106                         "?" + URLPARM_DEVTYPE + "=" + deviceType + modeParm + "&" + URLPARM_VERSION + "=" + version);
107             } else if (connection.equalsIgnoreCase(CONNECTION_TYPE_CUSTOM)) {
108                 // else custom -> don't modify url
109                 uri = url;
110                 url = URLPARM_URL + "=" + uri;
111             }
112             String updateUrl = url;
113
114             properties.put(ATTRIBUTE_VERSION, version);
115             properties.put(ATTRIBUTE_FW_URL, uri);
116             properties.put(ATTRIBUTE_UPDATE_URL, "http://" + getDeviceIp(properties) + "/ota?" + updateUrl);
117             properties.put(URLPARM_CONNECTION, connection);
118
119             if ("yes".equalsIgnoreCase(update)) {
120                 // do the update
121                 th.setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade");
122                 html += loadHTML(FWUPDATE2_HTML, properties);
123
124                 new Thread(() -> { // schedule asynchronous reboot
125                     try {
126                         ShellyApiInterface api = th.getApi();
127                         ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
128                         String status = getString(result.status);
129                         logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));
130
131                         // Shelly Motion needs almost 2min for upgrade
132                         scheduleUpdate(th, uid + "_upgrade", profile.isMotion ? 110 : 30);
133                     } catch (ShellyApiException e) {
134                         // maybe the device restarts before returning the http response
135                         logger.warn("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", e.toString()));
136                     }
137                 }).start();
138             } else {
139                 String message = getMessageP("fwupdate.confirm", MCINFO);
140                 properties.put(ATTRIBUTE_MESSAGE, message);
141                 html += loadHTML(FWUPDATE1_HTML, properties);
142             }
143         }
144
145         html += loadHTML(FOOTER_HTML, properties);
146         return new ShellyMgrResponse(html, HttpStatus.OK_200);
147     }
148
149     protected ShellyMgrResponse loadFirmware(String path, Map<String, String[]> parameters) throws ShellyApiException {
150         String deviceType = getUrlParm(parameters, URLPARM_DEVTYPE);
151         String deviceMode = getUrlParm(parameters, URLPARM_DEVMODE);
152         String version = getUrlParm(parameters, URLPARM_VERSION);
153         String url = getUrlParm(parameters, URLPARM_URL);
154         logger.info("ShellyManager: {}", getMessage("fwupdate.info", deviceType, version, url));
155
156         String failure = getMessage("fwupdate.notfound", deviceType, version, url);
157         try {
158             if (url.isEmpty()) {
159                 url = getFirmwareUrl("", deviceType, deviceMode, version, true);
160                 if (url.isEmpty()) {
161                     logger.warn("ShellyManager: {}", failure);
162                     return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
163                 }
164             }
165
166             logger.debug("ShellyManager: Loading firmware from {}", url);
167             // BufferedInputStream in = new BufferedInputStream(new URL(url).openStream());
168             // byte[] buf = new byte[in.available()];
169             // in.read(buf);
170             Request request = httpClient.newRequest(url).method(HttpMethod.GET).timeout(SHELLY_API_TIMEOUT_MS,
171                     TimeUnit.MILLISECONDS);
172             ContentResponse contentResponse = request.send();
173             HttpFields fields = contentResponse.getHeaders();
174             Map<String, String> headers = new TreeMap<>();
175             String etag = getString(fields.get("ETag"));
176             String ranges = getString(fields.get("accept-ranges"));
177             String modified = getString(fields.get("Last-Modified"));
178             headers.put("ETag", etag);
179             headers.put("accept-ranges", ranges);
180             headers.put("Last-Modified", modified);
181             byte[] data = contentResponse.getContent();
182             logger.info("ShellyManager: {}", getMessage("fwupdate.success", data.length, etag, modified));
183             return new ShellyMgrResponse(data, HttpStatus.OK_200, contentResponse.getMediaType(), headers);
184         } catch (ExecutionException | TimeoutException | InterruptedException | RuntimeException e) {
185             logger.info("ShellyManager: {}", failure, e);
186             return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
187
188         }
189     }
190
191     protected String getFirmwareUrl(String deviceIp, String deviceType, String mode, String version, boolean local)
192             throws ShellyApiException {
193         switch (version) {
194             case FWPROD:
195             case FWBETA:
196                 boolean prod = version.equals(FWPROD);
197                 if (!local) {
198                     // run regular device update
199                     return prod ? "update=true" : "beta=true";
200                 } else {
201                     // convert prod/beta to full url
202                     FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
203                     String url = getString(prod ? fw.url : fw.betaUrl);
204                     logger.debug("ShellyManager: Map {} release to url {}, version {}", url, prod ? fw.url : fw.betaUrl,
205                             prod ? fw.version : fw.betaVer);
206                     return url;
207                 }
208             default: // Update from firmware archive
209                 FwArchList list = getFirmwareArchiveList(deviceType);
210                 ArrayList<FwArchEntry> versions = list.versions;
211                 if (versions != null) {
212                     for (FwArchEntry e : versions) {
213                         String url = FWREPO_ARCFILE_URL + version + "/" + getString(e.file);
214                         if (getString(e.version).equalsIgnoreCase(version)) {
215                             return url;
216                         }
217                     }
218                 }
219         }
220         return "";
221     }
222 }