[shelly] Fix Plus/Pro Auth support (#15284)

* Reimplement Auth Digist to return response as http header field instead
of being part of post data; avoid sending Basic Auth for Gen2 and
/shelly

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels 2023-07-27 15:41:32 +02:00 committed by Kai Kreuzer
parent 91c008a06c
commit 7f6502d769
10 changed files with 150 additions and 78 deletions

View File

@ -997,7 +997,6 @@ You should calibrate the valve using the device Web UI or Shelly App before star
| ------- | --------------- | -------- | --------- | ------------------------------------------------------------------- | | ------- | --------------- | -------- | --------- | ------------------------------------------------------------------- |
| sensors | temperature | Number | yes | Current Temperature in °C | | sensors | temperature | Number | yes | Current Temperature in °C |
| | state | Contact | yes | Valve status: OPEN or CLOSED (position = 0) | | | state | Contact | yes | Valve status: OPEN or CLOSED (position = 0) |
| | open | Contact | yes | ON: "window is open" was detected, OFF: window is closed |
| | lastUpdate | DateTime | yes | Timestamp of the last update (any sensor value changed) | | | lastUpdate | DateTime | yes | Timestamp of the last update (any sensor value changed) |
| control | targetTemp | Number | no | Temperature in °C: 4=Low/Min; 5..30=target temperature;31=Hi/Max | | control | targetTemp | Number | no | Temperature in °C: 4=Low/Min; 5..30=target temperature;31=Hi/Max |
| | position | Dimmer | no | Set valve to manual mode (0..100%) disables auto-temp) | | | position | Dimmer | no | Set valve to manual mode (0..100%) disables auto-temp) |

View File

@ -291,7 +291,7 @@ public class ShellyBindingConstants {
public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+ public static final String SHELLY_API_MIN_FWCOIOT = "v1.6";// v1.6.0+
public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+ public static final String SHELLY_API_FWCOIOT2 = "v1.8";// CoAP 2 with FW 1.8+
public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature public static final String SHELLY_API_FW_110 = "v1.10"; // FW 1.10 or newer detected, activates some add feature
public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.2"; // Gen 2 minimum FW public static final String SHELLY2_API_MIN_FWVERSION = "v0.10.1"; // Gen 2 minimum FW
// Alarm types/messages // Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE"; public static final String ALARM_TYPE_NONE = "NONE";
@ -327,7 +327,7 @@ public class ShellyBindingConstants {
public static final int DIGITS_LUX = 0; public static final int DIGITS_LUX = 0;
public static final int DIGITS_PERCENT = 1; public static final int DIGITS_PERCENT = 1;
public static final int SHELLY_API_TIMEOUT_MS = 15000; public static final int SHELLY_API_TIMEOUT_MS = 10000;
public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec public static final int UPDATE_STATUS_INTERVAL_SECONDS = 3; // check for updates every x sec
public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed public static final int UPDATE_SKIP_COUNT = 20; // update every x triggers or when a key was pressed
public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed public static final int UPDATE_MIN_DELAY = 15;// update every x triggers or when a key was pressed

View File

@ -33,7 +33,7 @@ public class ShellyApiResult {
public String response = ""; public String response = "";
public int httpCode = -1; public int httpCode = -1;
public String httpReason = ""; public String httpReason = "";
public String authResponse = ""; public String authChallenge = "";
public ShellyApiResult() { public ShellyApiResult() {
} }

View File

@ -14,16 +14,21 @@ package org.openhab.binding.shelly.internal.api;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS; import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*; import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*; import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import javax.ws.rs.core.HttpHeaders;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
@ -32,6 +37,8 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface; import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
@ -49,8 +56,9 @@ import com.google.gson.Gson;
public class ShellyHttpClient { public class ShellyHttpClient {
private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class); private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);
public static final String HTTP_HEADER_AUTH = "Authorization"; public static final String HTTP_HEADER_AUTH = HttpHeaders.AUTHORIZATION;
public static final String HTTP_AUTH_TYPE_BASIC = "Basic"; public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
public static final String HTTP_AUTH_TYPE_DIGEST = "Digest";
public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8"; public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded"; public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";
@ -72,6 +80,7 @@ public class ShellyHttpClient {
this.thingName = thingName; this.thingName = thingName;
setConfig(thingName, config); setConfig(thingName, config);
this.httpClient = httpClient; this.httpClient = httpClient;
this.httpClient.setConnectTimeout(SHELLY_API_TIMEOUT_MS);
} }
public void initialize() throws ShellyApiException { public void initialize() throws ShellyApiException {
@ -103,7 +112,7 @@ public class ShellyHttpClient {
boolean timeout = false; boolean timeout = false;
while (retries > 0) { while (retries > 0) {
try { try {
apiResult = innerRequest(HttpMethod.GET, uri, ""); apiResult = innerRequest(HttpMethod.GET, uri, null, "");
if (timeout) { if (timeout) {
logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered, logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
apiResult.getUrl()); apiResult.getUrl());
@ -128,10 +137,15 @@ public class ShellyHttpClient {
} }
public String httpPost(String uri, String data) throws ShellyApiException { public String httpPost(String uri, String data) throws ShellyApiException {
return innerRequest(HttpMethod.POST, uri, data).response; return innerRequest(HttpMethod.POST, uri, null, data).response;
} }
private ShellyApiResult innerRequest(HttpMethod method, String uri, String data) throws ShellyApiException { public String httpPost(@Nullable Shelly2AuthChallenge auth, String data) throws ShellyApiException {
return innerRequest(HttpMethod.POST, SHELLYRPC_ENDPOINT, auth, data).response;
}
private ShellyApiResult innerRequest(HttpMethod method, String uri, @Nullable Shelly2AuthChallenge auth,
String data) throws ShellyApiException {
Request request = null; Request request = null;
String url = "http://" + config.deviceIp + uri; String url = "http://" + config.deviceIp + uri;
ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url); ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
@ -140,10 +154,24 @@ public class ShellyHttpClient {
request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS, request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
if (!config.password.isEmpty() && !getString(data).contains("\"auth\":{")) { if (!uri.equals(SHELLY_URL_DEVINFO) && !config.password.isEmpty()) { // not for /shelly or no password
String value = config.userId + ":" + config.password; // configured
request.header(HTTP_HEADER_AUTH, // Add Auth info
HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes())); // Gen 1: Basic Auth
// Gen 2: Digest Auth
String authHeader = "";
if (auth != null) { // only if we received an Auth challenge
authHeader = formatAuthResponse(uri,
buildAuthResponse(uri, auth, SHELLY2_AUTHDEF_USER, config.password));
} else {
if (!uri.equals(SHELLYRPC_ENDPOINT)) {
String bearer = config.userId + ":" + config.password;
authHeader = HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(bearer.getBytes());
}
}
if (!authHeader.isEmpty()) {
request.header(HTTP_HEADER_AUTH, authHeader);
}
} }
fillPostData(request, data); fillPostData(request, data);
logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders()); logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders());
@ -162,14 +190,14 @@ public class ShellyHttpClient {
apiResult.httpCode = message.error.code; apiResult.httpCode = message.error.code;
apiResult.response = message.error.message; apiResult.response = message.error.message;
if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) { if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\""); apiResult.authChallenge = getString(message.error.message).replaceAll("\\\"", "\"");
} }
} }
} }
HttpFields headers = contentResponse.getHeaders(); HttpFields headers = contentResponse.getHeaders();
String auth = headers.get(HttpHeader.WWW_AUTHENTICATE); String authChallenge = headers.get(HttpHeader.WWW_AUTHENTICATE);
if (!getString(auth).isEmpty()) { if (!getString(authChallenge).isEmpty()) {
apiResult.authResponse = auth; apiResult.authChallenge = authChallenge;
} }
// validate response, API errors are reported as Json // validate response, API errors are reported as Json
@ -191,6 +219,36 @@ public class ShellyHttpClient {
return apiResult; return apiResult;
} }
protected @Nullable Shelly2AuthRsp buildAuthResponse(String uri, @Nullable Shelly2AuthChallenge challenge,
String user, String password) throws ShellyApiException {
if (challenge == null) {
return null; // not required
}
if (!SHELLY2_AUTHTTYPE_DIGEST.equalsIgnoreCase(challenge.authType)
|| !SHELLY2_AUTHALG_SHA256.equalsIgnoreCase(challenge.algorithm)) {
throw new IllegalArgumentException("Unsupported Auth type/algorithm requested by device");
}
Shelly2AuthRsp response = new Shelly2AuthRsp();
response.username = user;
response.realm = challenge.realm;
response.nonce = challenge.nonce;
response.cnonce = Long.toHexString((long) Math.floor(Math.random() * 10e8));
response.nc = "00000001";
response.authType = challenge.authType;
response.algorithm = challenge.algorithm;
String ha1 = sha256(response.username + ":" + response.realm + ":" + password);
String ha2 = sha256(HttpMethod.POST + ":" + uri);// SHELLY2_AUTH_NOISE;
response.response = sha256(
ha1 + ":" + response.nonce + ":" + response.nc + ":" + response.cnonce + ":" + "auth" + ":" + ha2);
return response;
}
protected String formatAuthResponse(String uri, @Nullable Shelly2AuthRsp rsp) {
return rsp != null ? MessageFormat.format(HTTP_AUTH_TYPE_DIGEST
+ " username=\"{0}\", realm=\"{1}\", uri=\"{2}\", nonce=\"{3}\", cnonce=\"{4}\", nc=\"{5}\", qop=\"auth\",response=\"{6}\", algorithm=\"{7}\", ",
rsp.username, rsp.realm, uri, rsp.nonce, rsp.cnonce, rsp.nc, rsp.response, rsp.algorithm) : "";
}
/** /**
* Fill in POST data, set http headers * Fill in POST data, set http headers
* *

View File

@ -52,8 +52,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSe
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat; import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum; import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux; import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRequest; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRsp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigCover;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigInput;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
@ -92,7 +91,7 @@ public class Shelly2ApiClient extends ShellyHttpClient {
protected final ShellyStatusSensor sensorData = new ShellyStatusSensor(); protected final ShellyStatusSensor sensorData = new ShellyStatusSensor();
protected final ArrayList<ShellyRollerStatus> rollerStatus = new ArrayList<>(); protected final ArrayList<ShellyRollerStatus> rollerStatus = new ArrayList<>();
protected @Nullable ShellyThingInterface thing; protected @Nullable ShellyThingInterface thing;
protected @Nullable Shelly2AuthRequest authReq; protected @Nullable Shelly2AuthRsp authReq;
public Shelly2ApiClient(String thingName, ShellyThingInterface thing) { public Shelly2ApiClient(String thingName, ShellyThingInterface thing) {
super(thingName, thing); super(thingName, thing);
@ -793,23 +792,6 @@ public class Shelly2ApiClient extends ShellyHttpClient {
return request; return request;
} }
protected Shelly2AuthRequest buildAuthRequest(Shelly2AuthResponse authParm, String user, String realm,
String password) throws ShellyApiException {
Shelly2AuthRequest authReq = new Shelly2AuthRequest();
authReq.username = "admin";
authReq.realm = realm;
authReq.nonce = authParm.nonce;
authReq.cnonce = (long) Math.floor(Math.random() * 10e8);
authReq.nc = authParm.nc != null ? authParm.nc : 1;
authReq.authType = SHELLY2_AUTHTTYPE_DIGEST;
authReq.algorithm = SHELLY2_AUTHALG_SHA256;
String ha1 = sha256(authReq.username + ":" + authReq.realm + ":" + password);
String ha2 = SHELLY2_AUTH_NOISE;
authReq.response = sha256(
ha1 + ":" + authReq.nonce + ":" + authReq.nc + ":" + authReq.cnonce + ":" + "auth" + ":" + ha2);
return authReq;
}
protected String mapValue(Map<String, String> map, @Nullable String key) { protected String mapValue(Map<String, String> map, @Nullable String key) {
String value; String value;
boolean known = key != null && !key.isEmpty() && map.containsKey(key); boolean known = key != null && !key.isEmpty() && map.containsKey(key);

View File

@ -27,6 +27,8 @@ import com.google.gson.annotations.SerializedName;
* @author Markus Michels - Initial contribution * @author Markus Michels - Initial contribution
*/ */
public class Shelly2ApiJsonDTO { public class Shelly2ApiJsonDTO {
public static final String SHELLYRPC_ENDPOINT = "/rpc";
public static final String SHELLYRPC_METHOD_CLASS_SHELLY = "Shelly"; public static final String SHELLYRPC_METHOD_CLASS_SHELLY = "Shelly";
public static final String SHELLYRPC_METHOD_CLASS_SWITCH = "Switch"; public static final String SHELLYRPC_METHOD_CLASS_SWITCH = "Switch";
@ -1004,7 +1006,7 @@ public class Shelly2ApiJsonDTO {
public Object params; public Object params;
public String event; public String event;
public Object result; public Object result;
public Shelly2AuthRequest auth; public Shelly2AuthRsp auth;
public Shelly2RpcMessageError error; public Shelly2RpcMessageError error;
} }
@ -1022,17 +1024,27 @@ public class Shelly2ApiJsonDTO {
public Shelly2RpcMessageError error; public Shelly2RpcMessageError error;
} }
public static String SHELLY2_AUTHDEF_USER = "admin";
public static String SHELLY2_AUTHTTYPE_DIGEST = "digest"; public static String SHELLY2_AUTHTTYPE_DIGEST = "digest";
public static String SHELLY2_AUTHTTYPE_STRING = "string"; public static String SHELLY2_AUTHTTYPE_STRING = "string";
public static String SHELLY2_AUTHALG_SHA256 = "SHA-256"; public static String SHELLY2_AUTHALG_SHA256 = "SHA-256";
// = ':auth:'+HexHash("dummy_method:dummy_uri"); // = ':auth:'+HexHash("dummy_method:dummy_uri");
public static String SHELLY2_AUTH_NOISE = "6370ec69915103833b5222b368555393393f098bfbfbb59f47e0590af135f062"; public static String SHELLY2_AUTH_NOISE = "6370ec69915103833b5222b368555393393f098bfbfbb59f47e0590af135f062";
public static class Shelly2AuthRequest { public static class Shelly2AuthChallenge { // on 401 message contains the auth info
@SerializedName("auth_type")
public String authType;
public String nonce;
public String nc;
public String realm;
public String algorithm;
}
public static class Shelly2AuthRsp {
public String username; public String username;
public Long nonce; public String nonce;
public Long cnonce; public String cnonce;
public Integer nc; public String nc;
public String realm; public String realm;
public String algorithm; public String algorithm;
public String response; public String response;
@ -1040,15 +1052,6 @@ public class Shelly2ApiJsonDTO {
public String authType; public String authType;
} }
public static class Shelly2AuthResponse { // on 401 message contains the auth info
@SerializedName("auth_type")
public String authType;
public Long nonce;
public Integer nc;
public String realm;
public String algorithm;
}
// BTHome samples // BTHome samples
// BLU Button 1 // BLU Button 1
// {"component":"script:2", "id":2, "event":"oh-blu.scan_result", // {"component":"script:2", "id":2, "event":"oh-blu.scan_result",

View File

@ -55,7 +55,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortSta
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight; import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay; import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor; import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthChallenge;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2ConfigParms;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DeviceConfigSta;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult; import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
@ -81,6 +81,7 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptRe
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface; import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable; import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -99,7 +100,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
protected boolean initialized = false; protected boolean initialized = false;
private boolean discovery = false; private boolean discovery = false;
private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket(); private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
private Shelly2AuthResponse authInfo = new Shelly2AuthResponse(); private @Nullable Shelly2AuthChallenge authInfo;
/** /**
* Regular constructor - called by Thing handler * Regular constructor - called by Thing handler
@ -203,6 +204,11 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
ShellySettingsDevice device = getDeviceInfo(); ShellySettingsDevice device = getDeviceInfo();
profile.settings.device = device; profile.settings.device = device;
if (!getString(device.fw).isEmpty()) {
profile.fwDate = substringBefore(device.fw, "/");
profile.fwVersion = profile.status.update.oldVersion = "v" + substringAfter(device.fw, "/");
}
profile.hostname = device.hostname; profile.hostname = device.hostname;
profile.deviceType = device.type; profile.deviceType = device.type;
profile.mac = device.mac; profile.mac = device.mac;
@ -388,6 +394,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
if (ourId != -1) { if (ourId != -1) {
startScript(ourId, false); startScript(ourId, false);
enableScript(script, false); enableScript(script, false);
deleteScript(ourId);
logger.debug("{}: Script {} was disabledd, id={}", thingName, script, ourId); logger.debug("{}: Script {} was disabledd, id={}", thingName, script, ourId);
} }
return; return;
@ -439,8 +446,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
} }
if (upload && ourId != -1) { if (upload && ourId != -1) {
// Delete existing script // Delete existing script
logger.debug("{}: Delete existing script", thingName); deleteScript(ourId);
apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(ourId));
} }
if (upload) { if (upload) {
@ -479,10 +485,8 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
if (!running) { if (!running) {
running = startScript(ourId, true); running = startScript(ourId, true);
} logger.debug("{}: Script {} {}", thingName, script,
if (!discovery) { running ? "was successfully started" : "failed to start");
logger.info("{}: Script {} {}", thingName, script,
running ? "was successfully (re)started" : "failed to start");
} }
} catch (ShellyApiException e) { } catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult(); ShellyApiResult res = e.getApiResult();
@ -515,10 +519,25 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class); apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
return true; return true;
} catch (ShellyApiException e) { } catch (ShellyApiException e) {
logger.debug("{}: Unable to enable script {}", thingName, script, e);
return false; return false;
} }
} }
private boolean deleteScript(int id) {
if (id == -1) {
throw new IllegalArgumentException("Invalid Script Id");
}
try {
logger.debug("{}: Delete existing script with id{}", thingName, id);
apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(id));
return true;
} catch (ShellyApiException e) {
logger.debug("{}: Unable to delete script with id {}", thingName, id);
}
return false;
}
@Override @Override
public void onConnect(String deviceIp, boolean connected) { public void onConnect(String deviceIp, boolean connected) {
if (thing == null && thingTable != null) { if (thing == null && thingTable != null) {
@ -550,7 +569,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
if (message.error != null) { if (message.error != null) {
if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) { if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
// Save nonce for notification // Save nonce for notification
Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class); Shelly2AuthChallenge auth = gson.fromJson(message.error.message, Shelly2AuthChallenge.class);
if (auth != null && auth.realm == null) { if (auth != null && auth.realm == null) {
logger.debug("{}: Authentication data received: {}", thingName, message.error.message); logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
authInfo = auth; authInfo = auth;
@ -757,13 +776,17 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
status.update.hasUpdate = ds.sys.availableUpdates.stable != null; status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
if (ds.sys.availableUpdates.stable != null) { if (ds.sys.availableUpdates.stable != null) {
status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version); status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.newVersion) < 0;
} }
if (ds.sys.availableUpdates.beta != null) { if (ds.sys.availableUpdates.beta != null) {
status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version); status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
status.hasUpdate = new ShellyVersionDTO().compare(profile.fwVersion, status.update.betaVersion) < 0;
} }
} }
if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) { if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null)
{
List<Object> values = new ArrayList<>(); List<Object> values = new ArrayList<>();
String boot = getString(ds.sys.wakeUpReason.boot); String boot = getString(ds.sys.wakeUpReason.boot);
String cause = getString(ds.sys.wakeUpReason.cause); String cause = getString(ds.sys.wakeUpReason.cause);
@ -793,7 +816,6 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
return relayStatus; return relayStatus;
} }
@SuppressWarnings("null")
@Override @Override
public void setRelayTurn(int id, String turnMode) throws ShellyApiException { public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
ShellyDeviceProfile profile = getProfile(); ShellyDeviceProfile profile = getProfile();
@ -1128,35 +1150,32 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
Shelly2RpcBaseMessage req = buildRequest(method, params); Shelly2RpcBaseMessage req = buildRequest(method, params);
try { try {
reconnect(); // make sure WS is connected reconnect(); // make sure WS is connected
if (authInfo.realm != null) {
req.auth = buildAuthRequest(authInfo, config.userId, config.serviceName, config.password);
}
json = rpcPost(gson.toJson(req)); json = rpcPost(gson.toJson(req));
} catch (ShellyApiException e) { } catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult(); ShellyApiResult res = e.getApiResult();
String auth = getString(res.authResponse); String auth = getString(res.authChallenge);
if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) { if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
String[] options = auth.split(","); String[] options = auth.split(",");
authInfo = new Shelly2AuthChallenge();
for (String o : options) { for (String o : options) {
String key = substringBefore(o, "=").stripLeading().trim(); String key = substringBefore(o, "=").stripLeading().trim();
String value = substringAfter(o, "=").replaceAll("\"", "").trim(); String value = substringAfter(o, "=").replaceAll("\"", "").trim();
switch (key) { switch (key) {
case "Digest qop": case "Digest qop":
authInfo.authType = SHELLY2_AUTHTTYPE_DIGEST;
break; break;
case "realm": case "realm":
authInfo.realm = value; authInfo.realm = value;
break; break;
case "nonce": case "nonce":
authInfo.nonce = Long.parseLong(value, 16); // authInfo.nonce = Long.parseLong(value, 16);
authInfo.nonce = value;
break; break;
case "algorithm": case "algorithm":
authInfo.algorithm = value; authInfo.algorithm = value;
break; break;
} }
} }
authInfo.nc = 1;
req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
json = rpcPost(gson.toJson(req)); json = rpcPost(gson.toJson(req));
} else { } else {
throw e; throw e;
@ -1187,7 +1206,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
} }
private String rpcPost(String postData) throws ShellyApiException { private String rpcPost(String postData) throws ShellyApiException {
return httpPost("/rpc", postData); return httpPost(authInfo, postData);
} }
private void reconnect() throws ShellyApiException { private void reconnect() throws ShellyApiException {

View File

@ -114,7 +114,7 @@ public class Shelly2RpcSocket {
try { try {
disconnect(); // for safety disconnect(); // for safety
URI uri = new URI("ws://" + deviceIp + "/rpc"); URI uri = new URI("ws://" + deviceIp + SHELLYRPC_ENDPOINT);
ClientUpgradeRequest request = new ClientUpgradeRequest(); ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader(HttpHeaders.HOST, deviceIp); request.setHeader(HttpHeaders.HOST, deviceIp);
request.setHeader("Origin", "http://" + deviceIp); request.setHeader("Origin", "http://" + deviceIp);
@ -276,6 +276,8 @@ public class Shelly2RpcSocket {
e.event, e.data.name); e.event, e.data.name);
} }
} }
} else {
handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
} }
} }
} }

View File

@ -104,7 +104,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
private final ShellyChannelCache cache; private final ShellyChannelCache cache;
private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS; private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final boolean gen2; private boolean gen2 = false;
private final boolean blu; private final boolean blu;
protected boolean autoCoIoT = false; protected boolean autoCoIoT = false;
@ -1138,11 +1138,12 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
* @param mode Device mode (e.g. relay, roller) * @param mode Device mode (e.g. relay, roller)
*/ */
protected void changeThingType(String thingType, String mode) { protected void changeThingType(String thingType, String mode) {
ThingTypeUID thingTypeUID = ShellyThingCreator.getThingTypeUID(thingType, "", mode); String deviceType = substringBefore(thingType, "-");
ThingTypeUID thingTypeUID = ShellyThingCreator.getThingTypeUID(thingType, deviceType, mode);
if (!thingTypeUID.equals(THING_TYPE_SHELLYUNKNOWN)) { if (!thingTypeUID.equals(THING_TYPE_SHELLYUNKNOWN)) {
logger.debug("{}: Changing thing type to {}", getThing().getLabel(), thingTypeUID); logger.debug("{}: Changing thing type to {}", getThing().getLabel(), thingTypeUID);
Map<String, String> properties = editProperties(); Map<String, String> properties = editProperties();
properties.replace(PROPERTY_DEV_TYPE, thingType); properties.replace(PROPERTY_DEV_TYPE, deviceType);
properties.replace(PROPERTY_DEV_MODE, mode); properties.replace(PROPERTY_DEV_MODE, mode);
updateProperties(properties); updateProperties(properties);
changeThingType(thingTypeUID, getConfig()); changeThingType(thingTypeUID, getConfig());

View File

@ -35,10 +35,18 @@
<label>ShellyPlus 2 Relay</label> <label>ShellyPlus 2 Relay</label>
<description>@text/thing-type.shelly.shellyplus2-relay.description</description> <description>@text/thing-type.shelly.shellyplus2-relay.description</description>
<channel-groups> <channel-groups>
<channel-group id="relay1" typeId="relayChannel"/> <channel-group id="relay1" typeId="relayChannel">
<channel-group id="meter1" typeId="meter"/> <label>@text/channel-group-type.shelly.relayChannel1.label</label>
<channel-group id="relay2" typeId="relayChannel"/> </channel-group>
<channel-group id="meter2" typeId="meter"/> <channel-group id="meter1" typeId="meter">
<label>@text/channel-group-type.shelly.meter1.label</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel2.label</label>
</channel-group>
<channel-group id="meter2" typeId="meter">
<label>@text/channel-group-type.shelly.meter1.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/> <channel-group id="device" typeId="deviceStatus"/>
</channel-groups> </channel-groups>