mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
91c008a06c
commit
7f6502d769
@ -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) |
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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);
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user