2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.manager;
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.*;
19 import java.util.ArrayList;
20 import java.util.HashMap;
22 import java.util.TreeMap;
23 import java.util.concurrent.ExecutionException;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
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;
48 * {@link ShellyManagerOtaPage} implements the Shelly Manager's download proxy for images (load them from bundle)
50 * @author Markus Michels - Initial contribution
53 public class ShellyManagerOtaPage extends ShellyManagerPage {
54 protected final Logger logger = LoggerFactory.getLogger(ShellyManagerOtaPage.class);
56 public ShellyManagerOtaPage(ConfigurationAdmin configurationAdmin, ShellyTranslationProvider translationProvider,
57 HttpClient httpClient, String localIp, int localPort, ShellyHandlerFactory handlerFactory) {
58 super(configurationAdmin, translationProvider, httpClient, localIp, localPort, handlerFactory);
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);
66 return generatePage(path, parameters);
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);
80 Map<String, String> properties = new HashMap<>();
81 String html = loadHTML(HEADER_HTML, properties);
82 ShellyManagerInterface th = getThingHandlers().get(uid);
84 properties = fillProperties(new HashMap<>(), uid, th);
85 ShellyThingConfiguration config = getThingConfig(th, properties);
86 ShellyDeviceProfile profile = th.getProfile();
87 String deviceType = getDeviceType(properties);
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)) {
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=")) {
99 url = URLPARM_URL + "=" + uri;
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
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
110 url = URLPARM_URL + "=" + uri;
112 String updateUrl = url;
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);
119 if ("yes".equalsIgnoreCase(update)) {
121 th.setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING, "offline.status-error-fwupgrade");
122 html += loadHTML(FWUPDATE2_HTML, properties);
124 new Thread(() -> { // schedule asynchronous reboot
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));
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()));
139 String message = getMessageP("fwupdate.confirm", MCINFO);
140 properties.put(ATTRIBUTE_MESSAGE, message);
141 html += loadHTML(FWUPDATE1_HTML, properties);
145 html += loadHTML(FOOTER_HTML, properties);
146 return new ShellyMgrResponse(html, HttpStatus.OK_200);
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));
156 String failure = getMessage("fwupdate.notfound", deviceType, version, url);
159 url = getFirmwareUrl("", deviceType, deviceMode, version, true);
161 logger.warn("ShellyManager: {}", failure);
162 return new ShellyMgrResponse(failure, HttpStatus.BAD_REQUEST_400);
166 logger.debug("ShellyManager: Loading firmware from {}", url);
167 // BufferedInputStream in = new BufferedInputStream(new URL(url).openStream());
168 // byte[] buf = new byte[in.available()];
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);
191 protected String getFirmwareUrl(String deviceIp, String deviceType, String mode, String version, boolean local)
192 throws ShellyApiException {
196 boolean prod = version.equals(FWPROD);
198 // run regular device update
199 return prod ? "update=true" : "beta=true";
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);
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)) {