Skip to content

Commit

Permalink
[shelly] Fix Gen2 auth, improved security for Gen1 auth, improved dis…
Browse files Browse the repository at this point in the history
…covery (openhab#15898)

Signed-off-by: Markus Michels <[email protected]>
  • Loading branch information
markus7017 authored Nov 18, 2023
1 parent 608007c commit 45786fa
Show file tree
Hide file tree
Showing 18 changed files with 164 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ public boolean isConnectionError() {
|| exType == NoRouteToHostException.class;
}

public boolean isNoRouteToHost() {
return getCauseClass() == NoRouteToHostException.class;
}

public boolean isUnknownHost() {
return getCauseClass() == UnknownHostException.class;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
Expand Down Expand Up @@ -42,7 +43,8 @@ public interface ShellyApiInterface {

ShellySettingsDevice getDeviceInfo() throws ShellyApiException;

ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException;
ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice device)
throws ShellyApiException;

ShellySettingsStatus getStatus() throws ShellyApiException;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
Expand All @@ -47,32 +48,28 @@
@NonNullByDefault
public class ShellyDeviceProfile {
private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class);
private static final Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
private static final Pattern GEN1_VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+(-[a-z0-9]*)?");
private static final Pattern GEN2_VERSION_PATTERN = Pattern.compile("\\d+\\.\\d+\\.\\d+(-[a-fh-z0-9]*)?");

public boolean initialized = false; // true when initialized

public String thingName = "";
public String deviceType = "";
public boolean extFeatures = false;

public String settingsJson = "";
public ShellySettingsDevice device = new ShellySettingsDevice();
public ShellySettingsGlobal settings = new ShellySettingsGlobal();
public ShellySettingsStatus status = new ShellySettingsStatus();

public String hostname = "";
public String name = "";
public String model = "";
public String mode = "";
public boolean discoverable = true;
public boolean auth = false;
public boolean alwaysOn = true;
public boolean isGen2 = false;
public boolean isBlu = false;
public String gateway = "";

public String hwRev = "";
public String hwBatchId = "";
public String mac = "";
public String fwVersion = "";
public String fwDate = "";

Expand Down Expand Up @@ -118,10 +115,13 @@ public class ShellyDeviceProfile {
public ShellyDeviceProfile() {
}

public ShellyDeviceProfile initialize(String thingType, String jsonIn) throws ShellyApiException {
public ShellyDeviceProfile initialize(String thingType, String jsonIn, @Nullable ShellySettingsDevice device)
throws ShellyApiException {
Gson gson = new Gson();

initialized = false;
if (device != null) {
this.device = device;
}

initFromThingType(thingType);

Expand All @@ -141,36 +141,36 @@ public ShellyDeviceProfile initialize(String thingType, String jsonIn) throws Sh
settings = fromJson(gson, json, ShellySettingsGlobal.class);

// General settings
if (getString(device.hostname).isEmpty() && !getString(device.mac).isEmpty()) {
device.hostname = device.mac.length() >= 12 ? "shelly-" + device.mac.toUpperCase().substring(6, 11)
: "unknown";
}
name = getString(settings.name);
deviceType = getString(settings.device.type);
mac = getString(settings.device.mac);
hostname = !getString(settings.device.hostname).isEmpty() ? settings.device.hostname.toLowerCase()
: mac.length() >= 12 ? "shelly-" + mac.toUpperCase().substring(6, 11) : "unknown";
mode = getString(settings.mode).toLowerCase();
hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : "";
hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : "";
fwDate = substringBefore(settings.fw, "/");
fwVersion = extractFwVersion(settings.fw);
fwDate = substringBefore(device.fw, "-");
fwVersion = extractFwVersion(device.fw);
ShellyVersionDTO version = new ShellyVersionDTO();
extFeatures = version.compare(fwVersion, SHELLY_API_FW_110) >= 0;
discoverable = (settings.discoverable == null) || settings.discoverable;

String mode = getString(device.mode);
isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER);
inColor = isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);

numRelays = !isLight ? getInteger(settings.device.numOutputs) : 0;
numRelays = !isLight ? getInteger(device.numOutputs) : 0;
if ((numRelays > 0) && (settings.relays == null)) {
numRelays = 0;
}
hasRelays = (numRelays > 0) || isDimmer;
numRollers = getInteger(settings.device.numRollers);
numRollers = getInteger(device.numRollers);
numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0;

isEMeter = settings.emeters != null;
numMeters = !isEMeter ? getInteger(settings.device.numMeters) : getInteger(settings.device.numEMeters);
numMeters = !isEMeter ? getInteger(device.numMeters) : getInteger(device.numEMeters);
if ((numMeters == 0) && isLight) {
// RGBW2 doesn't report, but has one
numMeters = inColor ? 1 : getInteger(settings.device.numOutputs);
numMeters = inColor ? 1 : getInteger(device.numOutputs);
}

initialized = true;
Expand Down Expand Up @@ -199,8 +199,9 @@ public void initFromThingType(String name) {
isGen2 = isGeneration2(thingType);
isBlu = isBluSeries(thingType); // e.g. SBBT for BLU Button

isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2)
|| deviceType.equalsIgnoreCase(SHELLYDT_PLUSDIMMERUS)
String type = getString(device.type);
isDimmer = type.equalsIgnoreCase(SHELLYDT_DIMMER) || type.equalsIgnoreCase(SHELLYDT_DIMMER2)
|| type.equalsIgnoreCase(SHELLYDT_PLUSDIMMERUS)
|| thingType.equalsIgnoreCase(THING_TYPE_SHELLYPLUSDIMMERUS_STR);
isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
Expand Down Expand Up @@ -390,7 +391,8 @@ public static String extractFwVersion(@Nullable String version) {
.replace("/v1.12-", "/v1.12.0");

// Extract version from string, e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master
Matcher matcher = VERSION_PATTERN.matcher(vers);
Matcher matcher = version.startsWith("v") ? GEN1_VERSION_PATTERN.matcher(vers)
: GEN2_VERSION_PATTERN.matcher(vers);
if (matcher.find()) {
return matcher.group(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class ShellyHttpClient {
protected int timeoutErrors = 0;
protected int timeoutsRecovered = 0;
private ShellyDeviceProfile profile;
protected boolean basicAuth = false;

public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
this(thingName, thing.getThingConfig(), thing.getHttpClient());
Expand All @@ -83,9 +84,6 @@ public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpC
this.httpClient.setConnectTimeout(SHELLY_API_TIMEOUT_MS);
}

public void initialize() throws ShellyApiException {
}

public void setConfig(String thingName, ShellyThingConfiguration config) {
this.thingName = thingName;
this.config = config;
Expand Down Expand Up @@ -167,7 +165,7 @@ private ShellyApiResult innerRequest(HttpMethod method, String uri, @Nullable Sh
authHeader = formatAuthResponse(uri,
buildAuthResponse(uri, auth, SHELLY2_AUTHDEF_USER, config.password));
} else {
if (!uri.equals(SHELLYRPC_ENDPOINT)) {
if (basicAuth) {
String bearer = config.userId + ":" + config.password;
authHeader = HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(bearer.getBytes());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ public class Shelly1ApiJsonDTO {

public static class ShellySettingsDevice {
public String type;
public String mode; // Gen 1
public String id; // Gen2: service name
public String name; // Gen2: configured device name
public String profile; // Gen 2
public String mac;
public String hostname;
public String fw;
Expand Down Expand Up @@ -563,7 +567,6 @@ public static class ShellySettingsUpdate {

public static class ShellySettingsGlobal {
// https://shelly-api-docs.shelly.cloud/#shelly1pm-settings
public ShellySettingsDevice device = new ShellySettingsDevice();
@SerializedName("wifi_ap")
public ShellySettingsWiFiAp wifiAp = new ShellySettingsWiFiAp();
@SerializedName("wifi_sta")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ public void processResponse(@Nullable Response response) {
for (Option opt : options) {
if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
String devid = opt.getStringValue();
if (devid.contains("#") && profile.mac != null) {
if (devid.contains("#") && profile.device.mac != null) {
// Format: <device type>#<mac address>#<coap version>
String macid = substringBetween(devid, "#", "#");
if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
if (getString(profile.device.mac).toUpperCase().contains(macid.toUpperCase())) {
match = true;
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
Expand Down Expand Up @@ -79,10 +80,26 @@ public Shelly1HttpApi(String thingName, ShellyThingConfiguration config, HttpCli
this.profile = new ShellyDeviceProfile();
}

@Override
public void initialize() throws ShellyApiException {
profile.device = getDeviceInfo();
}

@Override
public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
ShellySettingsDevice info = callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
info.gen = 1;
basicAuth = getBool(info.auth);

if (getString(info.mode).isEmpty()) { // older Gen1 Firmware
if (getInteger(info.numRollers) > 0) {
info.mode = SHELLY_CLASS_ROLLER;
} else if (getInteger(info.numOutputs) > 0) {
info.mode = SHELLY_CLASS_RELAY;
} else {
info.mode = "";
}
}
return info;
}

Expand All @@ -104,18 +121,25 @@ public String getDebugLog(String id) throws ShellyApiException {
* @throws ShellyApiException
*/
@Override
public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
public ShellyDeviceProfile getDeviceProfile(String thingType, @Nullable ShellySettingsDevice device)
throws ShellyApiException {
if (device != null) {
profile.device = device;
}
if (profile.device.type == null) {
profile.device = getDeviceInfo();
}
String json = httpRequest(SHELLY_URL_SETTINGS);
if (json.contains("\"type\":\"SHDM-")) {
logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
json = fixDimmerJson(json);
}

// Map settings to device profile for Light and Sense
profile.initialize(thingType, json);
profile.initialize(thingType, json, profile.device);

// 2nd level initialization
profile.thingName = profile.hostname;
profile.thingName = profile.device.hostname;
if (profile.isLight && (profile.numMeters == 0)) {
logger.debug("{}: Get number of meters from light status", thingName);
ShellyStatusLight status = getLightStatus();
Expand Down Expand Up @@ -396,10 +420,10 @@ public String setCloud(boolean enabled) throws ShellyApiException {
*/
@Override
public void setLightMode(String mode) throws ShellyApiException {
if (!mode.isEmpty() && !profile.mode.equals(mode)) {
if (!mode.isEmpty() && !profile.device.mode.equals(mode)) {
setLightSetting(SHELLY_API_MODE, mode);
profile.mode = mode;
profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
profile.device.mode = mode;
profile.inColor = profile.isLight && mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ public Shelly2ApiClient(String thingName, ShellyThingConfiguration config, HttpC
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_STOPPED, SHELLY_RSTATE_STOP);
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CALIB, SHELLY2_RSTATE_CALIB); // Gen2-only
}
protected static final Map<String, String> MAP_PROFILE = new HashMap<>();
static {
MAP_PROFILE.put(SHELLY_CLASS_RELAY, SHELLY2_PROFILE_RELAY);
MAP_PROFILE.put(SHELLY_CLASS_ROLLER, SHELLY2_PROFILE_COVER);
}

protected @Nullable ArrayList<@Nullable ShellySettingsRelay> fillRelaySettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class Shelly2ApiJsonDTO {

// Component types
public static final String SHELLY2_PROFILE_RELAY = "switch";
public static final String SHELLY2_PROFILE_ROLLER = "cover";
public static final String SHELLY2_PROFILE_COVER = "cover";

// Button types/modes
public static final String SHELLY2_BTNT_MOMENTARY = "momentary";
Expand Down Expand Up @@ -183,13 +183,14 @@ public static class Shelly2DeviceSettings {
public String id;
public String mac;
public String model;
public String profile;
public Integer gen;
@SerializedName("fw_id")
public String firmware;
public String fw;
public String ver;
public String app;
@SerializedName("auth_en")
public Boolean authEnable;
public Boolean auth;
@SerializedName("auth_domain")
public String authDomain;
}
Expand Down
Loading

0 comments on commit 45786fa

Please sign in to comment.