[shelly] Add Plus/Pro support, some bugfixes (#13439)

* Plus/Pro support and some refactoring, bugfixes

Signed-off-by: Markus Michels <markus7017@gmail.com>

* Review changes applied

Signed-off-by: Markus Michels <markus7017@gmail.com>

* review changes

Signed-off-by: Markus Michels <markus7017@gmail.com>

Signed-off-by: Markus Michels <markus7017@gmail.com>
This commit is contained in:
Markus Michels
2022-10-04 08:00:50 +02:00
committed by GitHub
parent d2dde9768f
commit 6835c278e1
41 changed files with 4112 additions and 572 deletions

View File

@@ -35,18 +35,23 @@ public class ShellyBindingConstants {
public static final String BINDING_ID = "shelly";
public static final String SYSTEM_ID = "system";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1L, THING_TYPE_SHELLY1PM,
THING_TYPE_SHELLYEM, THING_TYPE_SHELLY3EM, THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY2_ROLLER,
THING_TYPE_SHELLY25_RELAY, THING_TYPE_SHELLY25_ROLLER, THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG,
THING_TYPE_SHELLYPLUGS, THING_TYPE_SHELLYPLUGU1, THING_TYPE_SHELLYUNI, THING_TYPE_SHELLYDIMMER,
THING_TYPE_SHELLYDIMMER2, THING_TYPE_SHELLYIX3, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO,
THING_TYPE_SHELLYVINTAGE, THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR,
THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT, THING_TYPE_SHELLYTRV, THING_TYPE_SHELLYSENSE,
THING_TYPE_SHELLYEYE, THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS, THING_TYPE_SHELLYFLOOD,
THING_TYPE_SHELLYDOORWIN, THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1,
THING_TYPE_SHELLYBUTTON2, THING_TYPE_SHELLMOTION, THING_TYPE_SHELLMOTION,
THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1L, THING_TYPE_SHELLY1PM, THING_TYPE_SHELLYEM,
THING_TYPE_SHELLY3EM, THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY25_RELAY,
THING_TYPE_SHELLY25_ROLLER, THING_TYPE_SHELLY4PRO, THING_TYPE_SHELLYPLUG, THING_TYPE_SHELLYPLUGS,
THING_TYPE_SHELLYPLUGU1, THING_TYPE_SHELLYUNI, THING_TYPE_SHELLYDIMMER, THING_TYPE_SHELLYDIMMER2,
THING_TYPE_SHELLYIX3, THING_TYPE_SHELLYBULB, THING_TYPE_SHELLYDUO, THING_TYPE_SHELLYVINTAGE,
THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR, THING_TYPE_SHELLYRGBW2_WHITE,
THING_TYPE_SHELLYHT, THING_TYPE_SHELLYTRV, THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE,
THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS, THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYDOORWIN,
THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, THING_TYPE_SHELLYBUTTON2,
THING_TYPE_SHELLMOTION, THING_TYPE_SHELLMOTION, THING_TYPE_SHELLYPLUS1, THING_TYPE_SHELLYPLUS1PM,
THING_TYPE_SHELLYPLUS2PM_RELAY, THING_TYPE_SHELLYPLUS2PM_ROLLER, THING_TYPE_SHELLYPRO1,
THING_TYPE_SHELLYPRO1PM, THING_TYPE_SHELLYPRO2_RELAY, THING_TYPE_SHELLYPRO2PM_RELAY,
THING_TYPE_SHELLYPRO2PM_ROLLER, THING_TYPE_SHELLYPRO3, THING_TYPE_SHELLYPRO4PM,
THING_TYPE_SHELLYPLUSI4, THING_TYPE_SHELLYPLUSI4DC, THING_TYPE_SHELLYPLUSHT,
THING_TYPE_SHELLYPLUSPLUGUS, THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN)
.collect(Collectors.toSet()));
// Thing Configuration Properties
public static final String CONFIG_DEVICEIP = "deviceIp";
@@ -218,6 +223,7 @@ public class ShellyBindingConstants {
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_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
// Alarm types/messages
public static final String ALARM_TYPE_NONE = "NONE";
@@ -238,7 +244,8 @@ public class ShellyBindingConstants {
public static final String EVENT_TYPE_SENSORDATA = "report";
// URI for the EventServlet
public static final String SHELLY_CALLBACK_URI = "/shelly/event";
public static final String SHELLY1_CALLBACK_URI = "/shelly/event";
public static final String SHELLY2_CALLBACK_URI = "/shelly/wsevent";
public static final int DIM_STEPSIZE = 5;

View File

@@ -12,7 +12,12 @@
*/
package org.openhab.binding.shelly.internal.api;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.NoRouteToHostException;
import java.net.PortUnreachableException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
@@ -65,19 +70,21 @@ public class ShellyApiException extends Exception {
@Override
public String toString() {
String message = nonNullString(super.getMessage());
String message = nonNullString(super.getMessage()).replace("java.util.concurrent.ExecutionException: ", "")
.replace("java.net.", "");
String cause = getCauseClass().toString();
String url = apiResult.getUrl();
if (!isEmpty()) {
if (isUnknownHost()) {
String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io
message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)",
string[1]);
} else if (isMalformedURL()) {
message = MessageFormat.format("Invalid URL: {0}", apiResult.getUrl());
message = "Invalid URL: " + url;
} else if (isTimeout()) {
message = MessageFormat.format("Device unreachable or API Timeout ({0})", apiResult.getUrl());
} else {
message = MessageFormat.format("{0} ({1})", message, cause);
message = "API Timeout for " + url;
} else if (!isConnectionError()) {
message = message + "(" + cause + ")";
}
} else {
message = apiResult.toString();
@@ -91,23 +98,30 @@ public class ShellyApiException extends Exception {
public boolean isTimeout() {
Class<?> extype = !isEmpty() ? getCauseClass() : null;
return (extype != null) && ((extype == TimeoutException.class) || (extype == ExecutionException.class)
|| (extype == InterruptedException.class)
return (extype != null) && ((extype == TimeoutException.class) || extype == InterruptedException.class
|| extype == SocketTimeoutException.class
|| nonNullString(getMessage()).toLowerCase().contains("timeout"));
}
public boolean isConnectionError() {
Class<?> exType = getCauseClass();
return isUnknownHost() || isMalformedURL() || exType == ConnectException.class
|| exType == SocketException.class || exType == PortUnreachableException.class
|| exType == NoRouteToHostException.class;
}
public boolean isUnknownHost() {
return getCauseClass() == UnknownHostException.class;
}
public boolean isMalformedURL() {
return getCauseClass() == MalformedURLException.class;
}
public boolean isHttpAccessUnauthorized() {
return apiResult.isHttpAccessUnauthorized();
}
public boolean isUnknownHost() {
return getCauseClass() == MalformedURLException.class;
}
public boolean isMalformedURL() {
return getCauseClass() == UnknownHostException.class;
}
public boolean isJSONException() {
return getCauseClass() == JsonSyntaxException.class;
}
@@ -126,6 +140,9 @@ public class ShellyApiException extends Exception {
private Class<?> getCauseClass() {
Throwable cause = getCause();
if (cause != null && cause.getClass() == ExecutionException.class) {
cause = cause.getCause();
}
if (cause != null) {
return cause.getClass();
}

View File

@@ -20,6 +20,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerSt
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
@@ -59,7 +60,7 @@ public interface ShellyApiInterface {
public void setRollerPos(int relayIndex, int position) throws ShellyApiException;
public void setTimer(int index, String timerName, int value) throws ShellyApiException;
public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException;
public ShellyStatusSensor getSensorStatus() throws ShellyApiException;
@@ -92,12 +93,20 @@ public interface ShellyApiInterface {
public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException;
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException;
public ShellySettingsLogin getLoginSettings() throws ShellyApiException;
public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException;
public String setWiFiRecovery(boolean enable) throws ShellyApiException;
public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException;
public boolean setEthernet(boolean enable) throws ShellyApiException;
public boolean setBluetooth(boolean enable) throws ShellyApiException;
public String deviceReboot() throws ShellyApiException;
public String setDebug(boolean enabled) throws ShellyApiException;
@@ -123,4 +132,6 @@ public interface ShellyApiInterface {
public void setActionURLs() throws ShellyApiException;
public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException;
public void close();
}

View File

@@ -112,15 +112,20 @@ public class ShellyDeviceProfile {
public ShellyDeviceProfile() {
}
public ShellyDeviceProfile initialize(String thingType, String json) throws ShellyApiException {
public ShellyDeviceProfile initialize(String thingType, String jsonIn) throws ShellyApiException {
Gson gson = new Gson();
initialized = false;
initFromThingType(thingType);
String json = jsonIn;
if (json.contains("\"ext_temperature\":{\"0\":[{")) {
// Shelly UNI uses ext_temperature array, reformat to avoid GSON exception
json = json.replace("ext_temperature", "ext_temperature_array");
}
settingsJson = json;
ShellySettingsGlobal gs = fromJson(gson, json, ShellySettingsGlobal.class);
settings = gs; // only update when no exception
settings = fromJson(gson, json, ShellySettingsGlobal.class);
// General settings
name = getString(settings.name);
@@ -282,6 +287,7 @@ public class ShellyDeviceProfile {
return "";
}
@SuppressWarnings("null")
public boolean inButtonMode(int idx) {
if (idx < 0) {
logger.debug("{}: Invalid index {} for inButtonMode()", thingName, idx);
@@ -323,8 +329,8 @@ public class ShellyDeviceProfile {
}
public int getRollerFav(int id) {
if ((id >= 0) && getBool(settings.favoritesEnabled) && (settings.favorites != null)
&& (id < settings.favorites.size())) {
if (id >= 0 && getBool(settings.favoritesEnabled) && settings.favorites != null
&& id < settings.favorites.size()) {
return settings.favorites.get(id).pos;
}
return -1;
@@ -348,8 +354,11 @@ public class ShellyDeviceProfile {
public static String extractFwVersion(@Nullable String version) {
if (version != null) {
// fix version e.g. 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
String vers = version.replace("/v.1.10-", "/v1.10.0-");
// fix version e.g.
// 20210319-122304/v.1.10-Dimmer1-gfd4cc10 (with v.1. instead of v1.)
// 20220809-125346/v1.12-g99f7e0b (.0 in 1.12.0 missing)
String vers = version.replace("/v.1.10-", "/v1.10.0-") //
.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);

View File

@@ -21,89 +21,86 @@ import java.util.Map;
import java.util.TreeMap;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api2.Shelly2RpcSocket;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ShellyEventServlet} implements a servlet. which is called by the Shelly device to signnal events (button,
* relay output, sensor data). The binding automatically sets those vent urls on startup (when not disabled in the thing
* config).
* {@link Shelly2RpcServlet} implements the WebSocket callback for Gen2 devices
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@WebServlet(name = "ShellyEventServlet", urlPatterns = { SHELLY1_CALLBACK_URI, SHELLY2_CALLBACK_URI })
@Component(service = HttpServlet.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
public class ShellyEventServlet extends HttpServlet {
private static final long serialVersionUID = 549582869577534569L;
public class ShellyEventServlet extends WebSocketServlet {
private static final long serialVersionUID = -1210354558091063207L;
private final Logger logger = LoggerFactory.getLogger(ShellyEventServlet.class);
private final HttpService httpService;
private final ShellyHandlerFactory handlerFactory;
private final ShellyThingTable thingTable;
@Activate
public ShellyEventServlet(@Reference HttpService httpService, @Reference ShellyHandlerFactory handlerFactory,
Map<String, Object> config) {
this.httpService = httpService;
public ShellyEventServlet(@Reference ShellyHandlerFactory handlerFactory, @Reference ShellyThingTable thingTable) {
this.handlerFactory = handlerFactory;
try {
httpService.registerServlet(SHELLY_CALLBACK_URI, this, null, httpService.createDefaultHttpContext());
logger.debug("ShellyEventServlet started at '{}'", SHELLY_CALLBACK_URI);
} catch (NamespaceException | ServletException | IllegalArgumentException e) {
logger.warn("Could not start CallbackServlet", e);
}
this.thingTable = thingTable;
logger.debug("Shelly EventServlet started at {} and {}", SHELLY1_CALLBACK_URI, SHELLY2_CALLBACK_URI);
}
@Deactivate
protected void deactivate() {
httpService.unregister(SHELLY_CALLBACK_URI);
logger.debug("ShellyEventServlet stopped");
logger.debug("ShellyEventServlet: Stopping");
}
/**
* Servlet handler. Shelly1: http request, Shelly2: WebSocket call
*/
@Override
protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse resp)
protected void service(HttpServletRequest request, HttpServletResponse resp)
throws ServletException, IOException, IllegalArgumentException {
String path = "";
String deviceName = "";
String index = "";
String type = "";
String path = getString(request.getRequestURI()).toLowerCase();
if ((request == null) || (resp == null)) {
logger.debug("request or resp must not be null!");
if (path.equals(SHELLY2_CALLBACK_URI)) { // Shelly2 WebSocket
super.service(request, resp);
return;
}
// Shelly1: http events, URL looks like
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyht-XXXXXX/sensordata?hum=53,temp=26.50
String deviceName = "";
String index = "";
String type = "";
try {
path = getString(request.getRequestURI()).toLowerCase();
String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
String ipAddress = request.getRemoteAddr();
Map<String, String[]> parameters = request.getParameterMap();
logger.debug("CallbackServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
logger.debug("ShellyEventServlet: {} Request from {}:{}{}?{}", request.getProtocol(), ipAddress,
request.getRemotePort(), path, parameters.toString());
if (!path.toLowerCase().startsWith(SHELLY_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("CallbackServlet received unknown request: path = {}", path);
if (!path.toLowerCase().startsWith(SHELLY1_CALLBACK_URI) || !path.contains("/event/shelly")) {
logger.warn("ShellyEventServlet received unknown request: path = {}", path);
return;
}
// URL looks like
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/relay/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyrelay-XXXXXX/roller/n?xxxxx or
// <ip address>:<remote port>/shelly/event/shellyht-XXXXXX/sensordata?hum=53,temp=26.50
deviceName = substringBetween(path, "/event/", "/").toLowerCase();
if (path.contains("/" + EVENT_TYPE_RELAY + "/") || path.contains("/" + EVENT_TYPE_ROLLER + "/")
|| path.contains("/" + EVENT_TYPE_LIGHT + "/")) {
@@ -114,8 +111,8 @@ public class ShellyEventServlet extends HttpServlet {
type = substringAfterLast(path, "/").toLowerCase();
}
logger.trace("{}: Process event of type type={}, index={}", deviceName, type, index);
Map<String, String> parms = new TreeMap<>();
Map<String, String> parms = new TreeMap<>();
for (Map.Entry<String, String[]> p : parameters.entrySet()) {
parms.put(p.getKey(), p.getValue()[0]);
@@ -129,4 +126,39 @@ public class ShellyEventServlet extends HttpServlet {
resp.getWriter().write("");
}
}
/*
* @Override
* public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
* {
* response.getWriter().println("HTTP GET method not implemented.");
* }
*/
/**
* WebSocket: register Shelly2RpcSocket class
*/
@Override
public void configure(@Nullable WebSocketServletFactory factory) {
if (factory != null) {
factory.getPolicy().setIdleTimeout(15000);
factory.setCreator(new Shelly2WebSocketCreator(thingTable));
factory.register(Shelly2RpcSocket.class);
}
}
public static class Shelly2WebSocketCreator implements WebSocketCreator {
private final Logger logger = LoggerFactory.getLogger(Shelly2WebSocketCreator.class);
private final ShellyThingTable thingTable;
public Shelly2WebSocketCreator(ShellyThingTable thingTable) {
this.thingTable = thingTable;
}
@Override
public Object createWebSocket(@Nullable ServletUpgradeRequest req, @Nullable ServletUpgradeResponse resp) {
logger.debug("WebSocket: Create socket from servlet");
return new Shelly2RpcSocket(thingTable, true);
}
}
}

View File

@@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.slf4j.Logger;
@@ -64,7 +65,6 @@ public class ShellyHttpClient {
public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
this(thingName, thing.getThingConfig(), thing.getHttpClient());
this.profile = thing.getProfile();
profile.initFromThingType(thingName);
}
public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
@@ -111,8 +111,9 @@ public class ShellyHttpClient {
}
return apiResult.response; // successful
} catch (ShellyApiException e) {
if ((!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound() || profile.hasBattery
|| (retries == 0)) {
if (e.isConnectionError()
|| (!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound()
|| profile.hasBattery || (retries == 0)) {
// Sensor in sleep mode or API exception for non-battery device or retry counter expired
throw e; // non-timeout exception
}
@@ -154,6 +155,16 @@ public class ShellyHttpClient {
String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
if (response.contains("\"error\":{")) { // Gen2
Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
if (message != null && message.error != null) {
apiResult.httpCode = message.error.code;
apiResult.response = message.error.message;
if (getInteger(message.error.code) == HttpStatus.UNAUTHORIZED_401) {
apiResult.authResponse = getString(message.error.message).replaceAll("\\\"", "\"");
}
}
}
HttpFields headers = contentResponse.getHeaders();
String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
if (!getString(auth).isEmpty()) {
@@ -171,7 +182,7 @@ public class ShellyHttpClient {
}
} catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
ShellyApiException ex = new ShellyApiException(apiResult, e);
if (!ex.isTimeout()) { // will be handled by the caller
if (!ex.isConnectionError() && !ex.isTimeout()) { // will be handled by the caller
logger.trace("{}: API call returned exception", thingName, ex);
}
throw ex;

View File

@@ -283,6 +283,7 @@ public class Shelly1ApiJsonDTO {
public Boolean enabled;
public String ssid;
public String key;
public Boolean rangeExtender; // Gen2 only
}
public static class ShellySettingsWiFiNetwork {
@@ -605,6 +606,7 @@ public class Shelly1ApiJsonDTO {
@SerializedName("max_power")
public Double maxPower;
public Boolean calibrated;
public Double voltage; // AC voltage for Shelly 2.5
@SerializedName("supply_voltage")
public Long supplyVoltage; // Shelly 1PM/1L: 0=110V, 1=220V
@@ -675,6 +677,10 @@ public class Shelly1ApiJsonDTO {
@SerializedName("sleep_time") // Shelly Motion
public Integer sleepTime;
// Gen2
public Boolean ethernet;
public Boolean bluetooth;
}
public static class ShellySettingsAttributes {
@@ -701,7 +707,8 @@ public class Shelly1ApiJsonDTO {
public String name; // FW 1.8: Symbolic Device name is configurable
@SerializedName("wifi_sta")
public ShellySettingsWiFiNetwork wifiSta = new ShellySettingsWiFiNetwork();
public ShellySettingsWiFiNetwork wifiSta = new ShellySettingsWiFiNetwork(); // WiFi client configuration. See
// /settings/sta for details
public ShellyStatusCloud cloud = new ShellyStatusCloud();
public ShellyStatusMqtt mqtt = new ShellyStatusMqtt();
@@ -715,13 +722,14 @@ public class Shelly1ApiJsonDTO {
public Integer cfgChangedCount; // FW 1.8
@SerializedName("actions_stats")
public ShellyActionsStats astats;
public Double voltage; // Shelly 2.5
public Integer input; // RGBW2 has no JSON array
public ArrayList<ShellySettingsRelay> relays;
public ArrayList<ShellyRollerStatus> rollers;
public ArrayList<ShellyShortLightStatus> dimmers;
public Double voltage; // Shelly 2.5
public Integer input; // RGBW2 has no JSON array
public ArrayList<ShellyInputState> inputs;
public ArrayList<ShellyShortLightStatus> dimmers;
public ArrayList<ShellyRollerStatus> rollers;
public ArrayList<ShellySettingsLight> lights;
public ArrayList<ShellySettingsMeter> meters;
public ArrayList<ShellySettingsEMeter> emeters;
@SerializedName("ext_temperature")
@@ -743,7 +751,6 @@ public class Shelly1ApiJsonDTO {
public ArrayList<ShellyThermnostat> thermostats;
public ShellySettingsUpdate update = new ShellySettingsUpdate();
@SerializedName("ram_total")
public Long ramTotal;
@SerializedName("ram_free")
@@ -798,7 +805,6 @@ public class Shelly1ApiJsonDTO {
public Boolean ison; // Whether output channel is on or off
public String mode; // color or white - valid only for Bulb and RGBW2 even Dimmer returns it also
public Integer brightness; // brightness: 0.100%
@SerializedName("has_timer")
public Boolean hasTimer;
}
@@ -914,6 +920,7 @@ public class Shelly1ApiJsonDTO {
public static class ShellyStatusSensor {
// https://shelly-api-docs.shelly.cloud/#h-amp-t-settings
public static class ShellySensorHum {
public Double value; // relative humidity in %
}
@@ -964,6 +971,7 @@ public class Shelly1ApiJsonDTO {
public static class ShellyExtTemperature {
public static class ShellyShortTemp {
public String hwID; // e.g. "2882379497020381",
public Double tC; // temperature in deg C
public Double tF; // temperature in deg F
}

View File

@@ -152,8 +152,7 @@ public class Shelly1CoIoTVersion1 extends Shelly1CoIoTProtocol implements Shelly
toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
break;
case "pf":
updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR,
toQuantityType(getDecimal(s.value), Units.PERCENT));
updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
break;
case "position":
// work around: Roller reports 101% instead max 100

View File

@@ -84,8 +84,6 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
private boolean updatesRequested = false;
private int coiotPort = COIOT_PORT;
private long coiotMessages = 0;
private long coiotErrors = 0;
private int lastSerial = -1;
private String lastPayload = "";
private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
@@ -164,7 +162,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
@Override
public void processResponse(@Nullable Response response) {
if (response == null) {
coiotErrors++;
thingHandler.incProtErrors();
return; // other device instance
}
ResponseCode code = response.getCode();
@@ -172,7 +170,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
// error handling
logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
response.getPayloadString());
coiotErrors++;
thingHandler.incProtErrors();
return;
}
@@ -205,14 +203,14 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
String uri = "";
int serial = -1;
try {
coiotMessages++;
thingHandler.incProtMessages();
if (logger.isDebugEnabled()) {
logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
}
if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
coiotErrors++;
thingHandler.incProtErrors();
return;
}
@@ -285,7 +283,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
coiotErrors++;
thingHandler.incProtErrors();
}
if (!updatesRequested) {
@@ -296,7 +294,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
} catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
resetSerial();
coiotErrors++;
thingHandler.incProtErrors();
}
}
@@ -500,6 +498,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
// Aggregate Meter Data from different Coap updates
int i = 1;
double totalCurrent = 0.0;
@SuppressWarnings("unused")
double totalKWH = 0.0;
boolean updateMeter = false;
while (i <= thingHandler.getProfile().numMeters) {
@@ -663,14 +662,6 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
coiotBound = false;
}
public long getMessageCount() {
return coiotMessages;
}
public long getErrorCount() {
return coiotErrors;
}
public void dispose() {
stop();
}

View File

@@ -151,6 +151,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
String json = "";
try {
json = httpRequest(SHELLY_URL_STATUS);
// Dimmer2 returns invalid json type for loaderror :-(
json = json.replace("\"loaderror\":0,", "\"loaderror\":false,")
.replace("\"loaderror\":1,", "\"loaderror\":true,")
@@ -223,18 +224,23 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
// SHelly H&T uses external_power, Sense uses charger
status.charger = profile.settings.externalPower != 0;
}
if (status.tmp != null && status.tmp.tC == null && status.tmp.value != null) { // Motion is is missing tC and tF
status.tmp.tC = getString(status.tmp.units).toUpperCase().equals(SHELLY_TEMP_FAHRENHEIT)
? ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(status.tmp.value).doubleValue()
: status.tmp.value;
}
return status;
}
@Override
public void setTimer(int index, String timerName, int value) throws ShellyApiException {
public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
String type = SHELLY_CLASS_RELAY;
if (profile.isRoller) {
type = SHELLY_CLASS_ROLLER;
} else if (profile.isLight) {
type = SHELLY_CLASS_LIGHT;
}
String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + (int) value;
httpRequest(uri);
}
@@ -351,11 +357,27 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
}
@Override
public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
return false;
}
@Override
public boolean setEthernet(boolean enable) throws ShellyApiException {
return false;
}
@Override
public boolean setBluetooth(boolean enable) throws ShellyApiException {
return false;
}
@Override
public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
return callApi("/sta_cache_reset", String.class);
}
@Override
public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
return callApi("/ota?" + uri, ShellySettingsUpdate.class);
}
@@ -559,7 +581,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
// H&T adds the type=xx to report_url itself, so we need to ommit here
String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY1_CALLBACK_URI + "/"
+ profile.thingName + "/" + eclass + urlParm;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
@@ -581,7 +603,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
throws ShellyApiException {
for (String eventType : eventTypes) {
if (profile.containsEventUrl(eventType)) {
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY1_CALLBACK_URI + "/"
+ profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
@@ -713,4 +735,8 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
public int getTimeoutsRecovered() {
return timeoutsRecovered;
}
@Override
public void close() {
}
}

View File

@@ -0,0 +1,548 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api2;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.CHANNEL_INPUT;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
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.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyFavPos;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorTmp;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRoller;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
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.ShellySensorBat;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorHum;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthRequest;
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.Shelly2DevConfigInput;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2DevConfigSwitch;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfig.Shelly2GetConfigResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2CoverStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusHumidity;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusPower;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult.Shelly2DeviceStatusTempId;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2InputStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RelayStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyComponents;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link Shelly2ApiClient} Low level part of the RPC API
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class Shelly2ApiClient extends ShellyHttpClient {
private final Logger logger = LoggerFactory.getLogger(Shelly2ApiClient.class);
protected final Random random = new Random();
protected final ShellyStatusRelay relayStatus = new ShellyStatusRelay();
protected final ShellyStatusSensor sensorData = new ShellyStatusSensor();
protected final ArrayList<ShellyRollerStatus> rollerStatus = new ArrayList<>();
protected @Nullable ShellyThingInterface thing;
protected @Nullable Shelly2AuthRequest authReq;
public Shelly2ApiClient(String thingName, ShellyThingInterface thing) {
super(thingName, thing);
this.thing = thing;
}
public Shelly2ApiClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
super(thingName, config, httpClient);
}
protected static final Map<String, String> MAP_INMODE_BTNTYPE = new HashMap<>();
static {
MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_MOMENTARY, SHELLY_BTNT_MOMENTARY);
MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_FLIP, SHELLY_BTNT_TOGGLE);
MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_FOLLOW, SHELLY_BTNT_EDGE);
MAP_INMODE_BTNTYPE.put(SHELLY2_BTNT_DETACHED, SHELLY_BTNT_MOMENTARY);
}
protected static final Map<String, String> MAP_INPUT_EVENT_TYPE = new HashMap<>();
static {
MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_1PUSH, SHELLY_BTNEVENT_1SHORTPUSH);
MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_2PUSH, SHELLY_BTNEVENT_2SHORTPUSH);
MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_3PUSH, SHELLY_BTNEVENT_3SHORTPUSH);
MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LPUSH, SHELLY_BTNEVENT_LONGPUSH);
MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LSPUSH, SHELLY_BTNEVENT_LONGSHORTPUSH);
MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_SLPUSH, SHELLY_BTNEVENT_SHORTLONGPUSH);
}
protected static final Map<String, String> MAP_INPUT_EVENT_ID = new HashMap<>();
static {
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_BTNUP, SHELLY_EVENT_BTN_OFF);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_BTNDOWN, SHELLY_EVENT_BTN_ON);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_1PUSH, SHELLY_EVENT_SHORTPUSH);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_2PUSH, SHELLY_EVENT_DOUBLE_SHORTPUSH);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_3PUSH, SHELLY_EVENT_TRIPLE_SHORTPUSH);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_LPUSH, SHELLY_EVENT_LONGPUSH);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_LSPUSH, SHELLY_EVENT_LONG_SHORTPUSH);
MAP_INPUT_EVENT_ID.put(SHELLY2_EVENT_SLPUSH, SHELLY_EVENT_SHORT_LONGTPUSH);
}
protected static final Map<String, String> MAP_INPUT_MODE = new HashMap<>();
static {
MAP_INPUT_MODE.put(SHELLY2_RMODE_SINGLE, SHELLY_INP_MODE_ONEBUTTON);
MAP_INPUT_MODE.put(SHELLY2_RMODE_DUAL, SHELLY_INP_MODE_OPENCLOSE);
MAP_INPUT_MODE.put(SHELLY2_RMODE_DETACHED, SHELLY_INP_MODE_ONEBUTTON);
}
protected static final Map<String, String> MAP_ROLLER_STATE = new HashMap<>();
static {
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_OPEN, SHELLY_RSTATE_OPEN);
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CLOSED, SHELLY_RSTATE_CLOSE);
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_OPENING, SHELLY2_RSTATE_OPENING); // Gen2-only
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CLOSING, SHELLY2_RSTATE_CLOSING); // Gen2-only
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_STOPPED, SHELLY_RSTATE_STOP);
MAP_ROLLER_STATE.put(SHELLY2_RSTATE_CALIB, SHELLY2_RSTATE_CALIB); // Gen2-only
}
protected @Nullable ArrayList<@Nullable ShellySettingsRelay> fillRelaySettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
if (dc.switch0 == null) {
return null;
}
ArrayList<@Nullable ShellySettingsRelay> relays = new ArrayList<>();
addRelaySettings(relays, dc.switch0);
addRelaySettings(relays, dc.switch1);
addRelaySettings(relays, dc.switch2);
addRelaySettings(relays, dc.switch3);
return relays;
}
private void addRelaySettings(ArrayList<@Nullable ShellySettingsRelay> relays,
@Nullable Shelly2DevConfigSwitch cs) {
if (cs == null) {
return;
}
ShellySettingsRelay rsettings = new ShellySettingsRelay();
rsettings.name = cs.name;
rsettings.ison = false;
rsettings.autoOn = getBool(cs.autoOn) ? cs.autoOnDelay : 0;
rsettings.autoOff = getBool(cs.autoOff) ? cs.autoOffDelay : 0;
rsettings.hasTimer = false;
rsettings.btnType = mapValue(MAP_INMODE_BTNTYPE, getString(cs.mode).toLowerCase());
relays.add(rsettings);
}
protected boolean fillDeviceStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult result,
boolean channelUpdate) throws ShellyApiException {
boolean updated = false;
updated |= updateInputStatus(status, result, channelUpdate);
updated |= updateRelayStatus(status, result.switch0, channelUpdate);
updated |= updateRelayStatus(status, result.switch1, channelUpdate);
updated |= updateRelayStatus(status, result.switch2, channelUpdate);
updated |= updateRelayStatus(status, result.switch3, channelUpdate);
updated |= updateRollerStatus(status, result.cover0, channelUpdate);
if (channelUpdate) {
updated |= ShellyComponents.updateMeters(getThing(), status);
}
updateHumidityStatus(sensorData, result.humidity0);
updateTemperatureStatus(sensorData, result.temperature0);
updateBatteryStatus(sensorData, result.devicepower0);
updated |= ShellyComponents.updateSensors(getThing(), status);
return updated;
}
private boolean updateRelayStatus(ShellySettingsStatus status, @Nullable Shelly2RelayStatus rs,
boolean channelUpdate) throws ShellyApiException {
if (rs == null) {
return false;
}
ShellyDeviceProfile profile = getProfile();
if (rs.id >= profile.numRelays) {
throw new IllegalArgumentException("Update for invalid relay index");
}
ShellySettingsRelay rstatus = status.relays.get(rs.id);
ShellyShortStatusRelay sr = relayStatus.relays.get(rs.id);
sr.isValid = rstatus.isValid = true;
sr.name = rstatus.name = status.name;
if (rs.output != null) {
sr.ison = rstatus.ison = getBool(rs.output);
}
if (getDouble(rs.timerStartetAt) > 0) {
int duration = (int) (now() - rs.timerStartetAt);
sr.timerRemaining = duration;
}
if (rs.temperature != null) {
status.tmp.isValid = true;
status.tmp.tC = rs.temperature.tC;
status.tmp.tF = rs.temperature.tF;
status.tmp.units = "C";
sr.temperature = getDouble(rs.temperature.tC);
if (status.temperature == null || getDouble(rs.temperature.tC) > status.temperature) {
status.temperature = sr.temperature;
}
} else {
status.tmp.isValid = false;
}
if (rs.voltage != null) {
if (status.voltage == null || rs.voltage > status.voltage) {
status.voltage = rs.voltage;
}
}
if (rs.errors != null) {
for (String error : rs.errors) {
sr.overpower = rstatus.overpower = SHELLY2_ERROR_OVERPOWER.equals(error);
status.overload = SHELLY2_ERROR_OVERVOLTAGE.equals(error);
status.overtemperature = SHELLY2_ERROR_OVERTEMP.equals(error);
}
sr.overtemperature = status.overtemperature;
}
ShellySettingsMeter sm = new ShellySettingsMeter();
ShellySettingsEMeter emeter = status.emeters.get(rs.id);
sm.isValid = emeter.isValid = true;
if (rs.apower != null) {
sm.power = emeter.power = rs.apower;
}
if (rs.aenergy != null) {
sm.total = emeter.total = rs.aenergy.total;
sm.counters = rs.aenergy.byMinute;
sm.timestamp = rs.aenergy.minuteTs;
}
if (rs.voltage != null) {
emeter.voltage = rs.voltage;
}
if (rs.current != null) {
emeter.current = rs.current;
}
if (rs.pf != null) {
emeter.pf = rs.pf;
}
// Update internal structures
status.relays.set(rs.id, rstatus);
status.meters.set(rs.id, sm);
status.emeters.set(rs.id, emeter);
relayStatus.relays.set(rs.id, sr);
relayStatus.meters.set(rs.id, sm);
return channelUpdate ? ShellyComponents.updateRelay((ShellyBaseHandler) getThing(), status, rs.id) : false;
}
protected @Nullable ArrayList<@Nullable ShellySettingsRoller> fillRollerSettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
if (dc.cover0 == null) {
return null;
}
ArrayList<@Nullable ShellySettingsRoller> rollers = new ArrayList<>();
addRollerSettings(rollers, dc.cover0);
fillRollerFavorites(profile, dc);
return rollers;
}
private void addRollerSettings(ArrayList<@Nullable ShellySettingsRoller> rollers,
@Nullable Shelly2DevConfigCover coverConfig) {
if (coverConfig == null) {
return;
}
ShellySettingsRoller settings = new ShellySettingsRoller();
settings.isValid = true;
settings.defaultState = coverConfig.initialState;
settings.inputMode = mapValue(MAP_INPUT_MODE, coverConfig.inMode);
settings.btnReverse = getBool(coverConfig.invertDirections) ? 1 : 0;
settings.swapInputs = coverConfig.swapInputs;
settings.maxtime = 0.0; // n/a
settings.maxtimeOpen = coverConfig.maxtimeOpen;
settings.maxtimeClose = coverConfig.maxtimeClose;
if (coverConfig.safetySwitch != null) {
settings.safetySwitch = coverConfig.safetySwitch.enable;
settings.safetyAction = coverConfig.safetySwitch.action;
}
if (coverConfig.obstructionDetection != null) {
settings.obstacleAction = coverConfig.obstructionDetection.action;
settings.obstacleDelay = coverConfig.obstructionDetection.holdoff.intValue();
settings.obstaclePower = coverConfig.obstructionDetection.powerThr;
}
rollers.add(settings);
}
private void fillRollerFavorites(ShellyDeviceProfile profile, Shelly2GetConfigResult dc) {
if (dc.sys.uiData.cover != null) {
String[] favorites = dc.sys.uiData.cover.split(",");
profile.settings.favorites = new ArrayList<>();
for (int i = 0; i < favorites.length; i++) {
ShellyFavPos fav = new ShellyFavPos();
fav.pos = Integer.parseInt(favorites[i]);
fav.name = fav.pos + "%";
profile.settings.favorites.add(fav);
}
profile.settings.favoritesEnabled = profile.settings.favorites.size() > 0;
logger.debug("{}: Roller Favorites loaded: {}", thingName,
profile.settings.favoritesEnabled ? profile.settings.favorites.size() : "none");
}
}
private boolean updateRollerStatus(ShellySettingsStatus status, @Nullable Shelly2CoverStatus cs,
boolean updateChannels) throws ShellyApiException {
if (cs == null) {
return false;
}
ShellyRollerStatus rs = status.rollers.get(cs.id);
ShellySettingsMeter sm = status.meters.get(cs.id);
ShellySettingsEMeter emeter = status.emeters.get(cs.id);
rs.isValid = sm.isValid = emeter.isValid = true;
if (cs.state != null) {
if (!getString(rs.state).equals(cs.state)) {
logger.debug("{}: Roller status changed from {} to {}, updateChannels={}", thingName, rs.state,
mapValue(MAP_ROLLER_STATE, cs.state), updateChannels);
}
rs.state = mapValue(MAP_ROLLER_STATE, cs.state);
rs.calibrating = SHELLY2_RSTATE_CALIB.equals(cs.state);
}
if (cs.currentPos != null) {
rs.currentPos = cs.currentPos;
}
if (cs.moveStartedAt != null) {
rs.duration = (int) (now() - cs.moveStartedAt.longValue());
}
if (cs.temperature != null && cs.temperature.tC > getDouble(status.temperature)) {
status.temperature = status.tmp.tC = getDouble(cs.temperature.tC);
}
if (cs.apower != null) {
rs.power = sm.power = emeter.power = cs.apower;
}
if (cs.aenergy != null) {
sm.total = emeter.total = cs.aenergy.total;
sm.counters = cs.aenergy.byMinute;
sm.timestamp = (long) cs.aenergy.minuteTs;
}
if (cs.voltage != null) {
emeter.voltage = cs.voltage;
}
if (cs.current != null) {
emeter.current = cs.current;
}
if (cs.pf != null) {
emeter.pf = cs.pf;
}
rollerStatus.set(cs.id, rs);
status.rollers.set(cs.id, rs);
relayStatus.meters.set(cs.id, sm);
status.meters.set(cs.id, sm);
status.emeters.set(cs.id, emeter);
postAlarms(cs.errors);
if (rs.calibrating != null && rs.calibrating) {
getThing().postEvent(SHELLY_EVENT_ROLLER_CALIB, false);
}
return updateChannels ? ShellyComponents.updateRoller((ShellyBaseHandler) getThing(), rs, cs.id) : false;
}
protected void updateHumidityStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusHumidity value) {
if (value == null) {
return;
}
if (sdata.hum == null) {
sdata.hum = new ShellySensorHum();
}
sdata.hum.value = getDouble(value.rh);
}
protected void updateTemperatureStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusTempId value) {
if (value == null) {
return;
}
if (sdata.tmp == null) {
sdata.tmp = new ShellySensorTmp();
}
sdata.tmp.isValid = true;
sdata.tmp.units = SHELLY_TEMP_CELSIUS;
sdata.tmp.tC = value.tC;
sdata.tmp.tF = value.tF;
}
protected void updateBatteryStatus(ShellyStatusSensor sdata, @Nullable Shelly2DeviceStatusPower value) {
if (value == null) {
return;
}
if (sdata.bat == null) {
sdata.bat = new ShellySensorBat();
}
if (value.battery != null) {
sdata.bat.voltage = value.battery.volt;
sdata.bat.value = value.battery.percent;
}
if (value.external != null) {
sdata.charger = value.external.present;
}
}
private void postAlarms(@Nullable ArrayList<@Nullable String> errors) throws ShellyApiException {
if (errors != null) {
for (String e : errors) {
if (e != null) {
getThing().postEvent(e, false);
}
}
}
}
protected @Nullable ArrayList<ShellySettingsInput> fillInputSettings(ShellyDeviceProfile profile,
Shelly2GetConfigResult dc) {
if (dc.input0 == null) {
return null; // device has no input
}
ArrayList<ShellySettingsInput> inputs = new ArrayList<>();
addInputSettings(inputs, dc.input0);
addInputSettings(inputs, dc.input1);
addInputSettings(inputs, dc.input2);
addInputSettings(inputs, dc.input3);
return inputs;
}
private void addInputSettings(ArrayList<ShellySettingsInput> inputs, @Nullable Shelly2DevConfigInput ic) {
if (ic == null) {
return;
}
ShellySettingsInput settings = new ShellySettingsInput();
settings.btnType = getString(ic.type).equalsIgnoreCase(SHELLY2_INPUTT_BUTTON) ? SHELLY_BTNT_MOMENTARY
: SHELLY_BTNT_EDGE;
inputs.add(settings);
}
protected boolean updateInputStatus(ShellySettingsStatus status, Shelly2DeviceStatusResult ds,
boolean updateChannels) throws ShellyApiException {
boolean updated = false;
updated |= addInputStatus(ds.input0, updateChannels);
updated |= addInputStatus(ds.input1, updateChannels);
updated |= addInputStatus(ds.input2, updateChannels);
updated |= addInputStatus(ds.input3, updateChannels);
status.inputs = relayStatus.inputs;
return updated;
}
private boolean addInputStatus(@Nullable Shelly2InputStatus is, boolean updateChannels) throws ShellyApiException {
if (is == null) {
return false;
}
ShellyDeviceProfile profile = getProfile();
if (is.id == null || is.id > profile.numInputs) {
logger.debug("{}: Invalid input id: {}", thingName, is.id);
return false;
}
String group = profile.getInputGroup(is.id);
ShellyInputState input = relayStatus.inputs.size() > is.id ? relayStatus.inputs.get(is.id)
: new ShellyInputState();
boolean updated = false;
input.input = getBool(is.state) ? 1 : 0; // old format Integer, new one Boolean
if (input.event == null && profile.inButtonMode(is.id)) {
input.event = "";
input.eventCount = 0;
}
relayStatus.inputs.set(is.id, input);
if (updateChannels) {
updated |= updateChannel(group, CHANNEL_INPUT + profile.getInputSuffix(is.id), getOnOff(getBool(is.state)));
}
return updated;
}
protected Shelly2RpcBaseMessage buildRequest(String method, @Nullable Object params) throws ShellyApiException {
Shelly2RpcBaseMessage request = new Shelly2RpcBaseMessage();
request.id = Math.abs(random.nextInt());
request.src = thingName;
request.method = !method.contains(".") ? SHELLYRPC_METHOD_CLASS_SHELLY + "." + method : method;
request.params = params;
request.auth = authReq;
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) {
String value;
boolean known = key != null && !key.isEmpty() && map.containsKey(key);
value = known ? getString(map.get(key)) : "";
logger.trace("{}: API value {} was mapped to {}", thingName, key, known ? value : "UNKNOWN");
return value;
}
protected boolean updateChannel(String group, String channel, State value) throws ShellyApiException {
return getThing().updateChannel(group, channel, value);
}
protected ShellyThingInterface getThing() throws ShellyApiException {
ShellyThingInterface t = thing;
if (t != null) {
return t;
}
throw new ShellyApiException("Thing/profile not initialized!");
}
ShellyDeviceProfile getProfile() throws ShellyApiException {
if (thing != null) {
return thing.getProfile();
}
throw new ShellyApiException("Unable to get profile, thing not initialized!");
}
}

View File

@@ -0,0 +1,763 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api2;
import java.util.ArrayList;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage.Shelly2RpcMessageError;
import com.google.gson.annotations.SerializedName;
/**
* {@link Shelly2ApiJsonDTO} wraps the Shelly REST API and provides various low level function to access the device api
* (not
* cloud api).
*
* @author Markus Michels - Initial contribution
*/
public class Shelly2ApiJsonDTO {
public static final String SHELLYRPC_METHOD_CLASS_SHELLY = "Shelly";
public static final String SHELLYRPC_METHOD_CLASS_SWITCH = "Switch";
public static final String SHELLYRPC_METHOD_GETDEVCONFIG = "GetDeviceInfo";
public static final String SHELLYRPC_METHOD_GETSYSCONFIG = "GetSysConfig"; // only sys
public static final String SHELLYRPC_METHOD_GETCONFIG = "GetConfig"; // sys + components
public static final String SHELLYRPC_METHOD_GETSYSSTATUS = "GetSysStatus"; // only sys
public static final String SHELLYRPC_METHOD_GETSTATUS = "GetStatus"; // sys + components
public static final String SHELLYRPC_METHOD_REBOOT = "Shelly.Reboot";
public static final String SHELLYRPC_METHOD_RESET = "Shelly.FactoryReset";
public static final String SHELLYRPC_METHOD_CHECKUPD = "Shelly.CheckForUpdate";
public static final String SHELLYRPC_METHOD_UPDATE = "Shelly.Update";
public static final String SHELLYRPC_METHOD_AUTHSET = "Shelly.SetAuth";
public static final String SHELLYRPC_METHOD_SWITCH_STATUS = "Switch.GetStatus";
public static final String SHELLYRPC_METHOD_SWITCH_SET = "Switch.Set";
public static final String SHELLYRPC_METHOD_SWITCH_SETCONFIG = "Switch.SetConfig";
public static final String SHELLYRPC_METHOD_COVER_SETPOS = "Cover.GoToPosition";
public static final String SHELLY2_COVER_CMD_OPEN = "Open";
public static final String SHELLY2_COVER_CMD_CLOSE = "Close";
public static final String SHELLY2_COVER_CMD_STOP = "Stop";
public static final String SHELLYRPC_METHOD_WIFIGETCONG = "Wifi.GetConfig";
public static final String SHELLYRPC_METHOD_WIFISETCONG = "Wifi.SetConfig";
public static final String SHELLYRPC_METHOD_ETHGETCONG = "Eth.GetConfig";
public static final String SHELLYRPC_METHOD_ETHSETCONG = "Eth.SetConfig";
public static final String SHELLYRPC_METHOD_BLEGETCONG = "BLE.GetConfig";
public static final String SHELLYRPC_METHOD_BLESETCONG = "BLE.SetConfig";
public static final String SHELLYRPC_METHOD_CLOUDSET = "Cloud.SetConfig";
public static final String SHELLYRPC_METHOD_WSGETCONFIG = "WS.GetConfig";
public static final String SHELLYRPC_METHOD_WSSETCONFIG = "WS.SetConfig";
public static final String SHELLYRPC_METHOD_NOTIFYSTATUS = "NotifyStatus"; // inbound status
public static final String SHELLYRPC_METHOD_NOTIFYFULLSTATUS = "NotifyFullStatus"; // inbound status from bat device
public static final String SHELLYRPC_METHOD_NOTIFYEVENT = "NotifyEvent"; // inbound event
// Component types
public static final String SHELLY2_PROFILE_RELAY = "switch";
public static final String SHELLY2_PROFILE_ROLLER = "cover";
// Button types/modes
public static final String SHELLY2_BTNT_MOMENTARY = "momentary";
public static final String SHELLY2_BTNT_FLIP = "flip";
public static final String SHELLY2_BTNT_FOLLOW = "follow";
public static final String SHELLY2_BTNT_DETACHED = "detached";
// Input types
public static final String SHELLY2_INPUTT_SWITCH = "switch";
public static final String SHELLY2_INPUTT_BUTTON = "button";
// Switcm modes
public static final String SHELLY2_API_MODE_DETACHED = "detached";
public static final String SHELLY2_API_MODE_FOLLOW = "follow";
// Initial switch states
public static final String SHELLY2_API_ISTATE_ON = "on";
public static final String SHELLY2_API_ISTATE_OFF = "off";
public static final String SHELLY2_API_ISTATE_FOLLOWLAST = "restore_last";
public static final String SHELLY2_API_ISTATE_MATCHINPUT = "match_input";
// Cover/Roller modes
public static final String SHELLY2_RMODE_SINGLE = "single";
public static final String SHELLY2_RMODE_DUAL = "dual";
public static final String SHELLY2_RMODE_DETACHED = "detached";
public static final String SHELLY2_RSTATE_OPENING = "opening";
public static final String SHELLY2_RSTATE_OPEN = "open";
public static final String SHELLY2_RSTATE_CLOSING = "closing";
public static final String SHELLY2_RSTATE_CLOSED = "closed";
public static final String SHELLY2_RSTATE_STOPPED = "stopped";
public static final String SHELLY2_RSTATE_CALIB = "calibrating";
// Event notifications
public static final String SHELLY2_EVENT_BTNUP = "btn_up";
public static final String SHELLY2_EVENT_BTNDOWN = "btn_down";
public static final String SHELLY2_EVENT_1PUSH = "single_push";
public static final String SHELLY2_EVENT_2PUSH = "double_push";
public static final String SHELLY2_EVENT_3PUSH = "triple_push";
public static final String SHELLY2_EVENT_LPUSH = "long_push";
public static final String SHELLY2_EVENT_SLPUSH = "short_long_push";
public static final String SHELLY2_EVENT_LSPUSH = "long_short_push";
public static final String SHELLY2_EVENT_SLEEP = "sleep";
public static final String SHELLY2_EVENT_CFGCHANGED = "config_changed";
public static final String SHELLY2_EVENT_OTASTART = "ota_begin";
public static final String SHELLY2_EVENT_OTAPROGRESS = "ota_progress";
public static final String SHELLY2_EVENT_OTADONE = "ota_success";
public static final String SHELLY2_EVENT_WIFICONNFAILED = "sta_connect_fail";
public static final String SHELLY2_EVENT_WIFIDISCONNECTED = "sta_disconnected";
// Error Codes
public static final String SHELLY2_ERROR_OVERPOWER = "overpower";
public static final String SHELLY2_ERROR_OVERTEMP = "overtemp";
public static final String SHELLY2_ERROR_OVERVOLTAGE = "overvoltage";
// Wakeup reasons (e.g. Plus HT)
public static final String SHELLY2_WAKEUPO_BOOT_POWERON = "poweron";
public static final String SHELLY2_WAKEUPO_BOOT_RESTART = "software_restart";
public static final String SHELLY2_WAKEUPO_BOOT_WAKEUP = "deepsleep_wake";
public static final String SHELLY2_WAKEUPO_BOOT_INTERNAL = "internal";
public static final String SHELLY2_WAKEUPO_BOOT_UNKNOWN = "unknown";
public static final String SHELLY2_WAKEUPOCAUSE_BUTTON = "button";
public static final String SHELLY2_WAKEUPOCAUSE_USB = "usb";
public static final String SHELLY2_WAKEUPOCAUSE_PERIODIC = "periodic";
public static final String SHELLY2_WAKEUPOCAUSE_UPDATE = "status_update";
public static final String SHELLY2_WAKEUPOCAUSE_UNDEFINED = "undefined";
public class Shelly2DevConfigBle {
public Boolean enable;
}
public class Shelly2DevConfigEth {
public Boolean enable;
public String ipv4mode;
public String ip;
public String netmask;
public String gw;
public String nameserver;
}
public static class Shelly2DeviceSettings {
public String name;
public String id;
public String mac;
public String model;
public Integer gen;
@SerializedName("fw_id")
public String firmware;
public String ver;
public String app;
@SerializedName("auth_en")
public Boolean authEnable;
@SerializedName("auth_domain")
public String authDomain;
}
public static class Shelly2DeviceConfigAp {
public static class Shelly2DeviceConfigApRE {
public Boolean enable;
}
public Boolean enable;
public String ssid;
public String password;
@SerializedName("is_open")
public Boolean isOpen;
@SerializedName("range_extender")
Shelly2DeviceConfigApRE rangeExtender;
}
public static class Shelly2DeviceConfig {
public class Shelly2DeviceConfigSys {
public class Shelly2DeviceConfigDevice {
public String name;
public String mac;
@SerializedName("fw_id")
public String fwId;
public String profile;
@SerializedName("eco_mode")
public Boolean ecoMode;
public Boolean discoverable;
}
public class Shelly2DeviceConfigLocation {
public String tz;
public Double lat;
public Double lon;
}
public class Shelly2DeviceConfigSntp {
public String server;
}
public class Shelly2DeviceConfigSleep {
@SerializedName("wakeup_period")
public Integer wakeupPeriod;
}
public class Shelly2DeviceConfigDebug {
public class Shelly2DeviceConfigDebugMqtt {
public Boolean enable;
}
public class Shelly2DeviceConfigDebugWebSocket {
public Boolean enable;
}
public class Shelly2DeviceConfigDebugUdp {
public String addr;
}
public Shelly2DeviceConfigDebugMqtt mqtt;
public Shelly2DeviceConfigDebugWebSocket websocket;
public Shelly2DeviceConfigDebugUdp udp;
}
public class Shelly2DeviceConfigUiData {
public String cover; // hold comma seperated list of roller favorites
}
public class Shelly2DeviceConfigRpcUdp {
@SerializedName("dst_addr")
public String dstAddr;
@SerializedName("listenPort")
public String listenPort;
}
@SerializedName("cfg_rev")
public Integer cfgRevision;
public Shelly2DeviceConfigDevice device;
public Shelly2DeviceConfigLocation location;
public Shelly2DeviceConfigSntp sntp;
public Shelly2DeviceConfigSleep sleep;
public Shelly2DeviceConfigDebug debug;
@SerializedName("ui_data")
public Shelly2DeviceConfigUiData uiData;
@SerializedName("rpc_udp")
public Shelly2DeviceConfigRpcUdp rpcUdp;
}
public class Shelly2DevConfigInput {
public String id;
public String name;
public String type;
public Boolean invert;
@SerializedName("factory_reset")
public Boolean factoryReset;
}
public class Shelly2DevConfigSwitch {
public String id;
public String name;
@SerializedName("in_mode")
public String mode;
@SerializedName("initial_state")
public String initialState;
@SerializedName("auto_on")
public Boolean autoOn;
@SerializedName("auto_on_delay")
public Double autoOnDelay;
@SerializedName("auto_off")
public Boolean autoOff;
@SerializedName("auto_off_delay")
public Double autoOffDelay;
@SerializedName("power_limit")
public Integer powerLimit;
@SerializedName("voltage_limit")
public Integer voltageLimit;
@SerializedName("current_limit")
public Double currentLimit;
}
public class Shelly2DevConfigCover {
public class Shelly2DeviceConfigCoverMotor {
@SerializedName("idle_power_thr")
public Double idle_powerThr;
}
public class Shelly2DeviceConfigCoverSafetySwitch {
public Boolean enable;
public String direction;
public String action;
@SerializedName("allowed_move")
public String allowedMove;
}
public class Shelly2DeviceConfigCoverObstructionDetection {
public Boolean enable;
public String direction;
public String action;
@SerializedName("power_thr")
public Integer powerThr;
public Double holdoff;
}
public String id;
public String name;
public Shelly2DeviceConfigCoverMotor motor;
@SerializedName("maxtime_open")
public Double maxtimeOpen;
@SerializedName("maxtime_close")
public Double maxtimeClose;
@SerializedName("initial_state")
public String initialState;
@SerializedName("invert_directions")
public Boolean invertDirections;
@SerializedName("in_mode")
public String inMode;
@SerializedName("swap_inputs")
public Boolean swapInputs;
@SerializedName("safety_switch")
public Shelly2DeviceConfigCoverSafetySwitch safetySwitch;
@SerializedName("power_limit")
public Integer powerLimit;
@SerializedName("voltage_limit")
public Integer voltageLimit;
@SerializedName("current_limit")
public Double currentLimit;
@SerializedName("obstruction_detection")
public Shelly2DeviceConfigCoverObstructionDetection obstructionDetection;
}
public static class Shelly2GetConfigResult {
public class Shelly2DevConfigCloud {
public Boolean enable;
public String server;
}
public class Shelly2DevConfigMqtt {
public Boolean enable;
public String server;
public String user;
@SerializedName("topic_prefix:0")
public String topicPrefix;
@SerializedName("rpc_ntf")
public String rpcNtf;
@SerializedName("status_ntf")
public String statusNtf;
}
public Shelly2DevConfigBle ble;
public Shelly2DevConfigEth eth;
public Shelly2DevConfigCloud cloud;
public Shelly2DevConfigMqtt mqtt;
public Shelly2DeviceConfigSys sys;
public Shelly2DeviceConfigWiFi wifi;
@SerializedName("input:0")
public Shelly2DevConfigInput input0;
@SerializedName("input:1")
public Shelly2DevConfigInput input1;
@SerializedName("input:2")
public Shelly2DevConfigInput input2;
@SerializedName("input:3")
public Shelly2DevConfigInput input3;
@SerializedName("switch:0")
public Shelly2DevConfigSwitch switch0;
@SerializedName("switch:1")
public Shelly2DevConfigSwitch switch1;
@SerializedName("switch:2")
public Shelly2DevConfigSwitch switch2;
@SerializedName("switch:3")
public Shelly2DevConfigSwitch switch3;
@SerializedName("cover:0")
public Shelly2DevConfigCover cover0;
}
public class Shelly2DeviceConfigSta {
public String ssid;
public String password;
@SerializedName("is_open")
public Boolean isOpen;
public Boolean enable;
public String ipv4mode;
public String ip;
public String netmask;
public String gw;
public String nameserver;
}
public class Shelly2DeviceConfigRoam {
@SerializedName("rssi_thr")
public Integer rssiThr;
public Integer interval;
}
public class Shelly2DeviceConfigWiFi {
public Shelly2DeviceConfigAp ap;
public Shelly2DeviceConfigSta sta;
public Shelly2DeviceConfigSta sta1;
public Shelly2DeviceConfigRoam roam;
}
public String id;
public String src;
public Shelly2GetConfigResult result;
}
public static class Shelly2DeviceStatus {
public class Shelly2InputStatus {
public Integer id;
public Boolean state;
}
public static class Shelly2DeviceStatusResult {
public class Shelly2DeviceStatusBle {
}
public class Shelly2DeviceStatusCloud {
public Boolean connected;
}
public class Shelly2DeviceStatusMqqt {
public Boolean connected;
}
public class Shelly2CoverStatus {
public Integer id;
public String source;
public String state;
public Double apower;
public Double voltage;
public Double current;
public Double pf;
public Shelly2Energy aenergy;
@SerializedName("current_pos")
public Integer currentPos;
@SerializedName("target_pos")
public Integer targetPos;
@SerializedName("move_timeout")
public Double moveTimeout;
@SerializedName("move_started_at")
public Double moveStartedAt;
@SerializedName("pos_control")
public Boolean posControl;
public Shelly2DeviceStatusTemp temperature;
public ArrayList<String> errors;
}
public class Shelly2DeviceStatusHumidity {
public Integer id;
public Double rh;
}
public class Shelly2DeviceStatusTempId extends Shelly2DeviceStatusTemp {
public Integer id;
}
public static class Shelly2DeviceStatusPower {
public static class Shelly2DeviceStatusBattery {
@SerializedName("V")
public Double volt;
public Double percent;
}
public static class Shelly2DeviceStatusCharger {
public Boolean present;
}
public Integer id;
public Shelly2DeviceStatusBattery battery;
public Shelly2DeviceStatusCharger external;
}
public Shelly2DeviceStatusBle ble;
public Shelly2DeviceStatusCloud cloud;
public Shelly2DeviceStatusMqqt mqtt;
public Shelly2DeviceStatusSys sys;
public Shelly2DeviceStatusSysWiFi wifi;
@SerializedName("input:0")
public Shelly2InputStatus input0;
@SerializedName("input:1")
public Shelly2InputStatus input1;
@SerializedName("input:2")
public Shelly2InputStatus input2;
@SerializedName("input:3")
public Shelly2InputStatus input3;
@SerializedName("switch:0")
public Shelly2RelayStatus switch0;
@SerializedName("switch:1")
public Shelly2RelayStatus switch1;
@SerializedName("switch:2")
public Shelly2RelayStatus switch2;
@SerializedName("switch:3")
public Shelly2RelayStatus switch3;
@SerializedName("cover:0")
public Shelly2CoverStatus cover0;
@SerializedName("humidity:0")
public Shelly2DeviceStatusHumidity humidity0;
@SerializedName("temperature:0")
public Shelly2DeviceStatusTempId temperature0;
@SerializedName("devicepower:0")
public Shelly2DeviceStatusPower devicepower0;
}
public class Shelly2DeviceStatusSys {
public class Shelly2DeviceStatusSysAvlUpdate {
public class Shelly2DeviceStatusSysUpdate {
public String version;
}
public Shelly2DeviceStatusSysUpdate stable;
public Shelly2DeviceStatusSysUpdate beta;
}
public class Shelly2DeviceStatusWakeup {
public String boot;
public String cause;
}
public String mac;
@SerializedName("restart_required")
public Boolean restartRequired;
public String time;
public Long unixtime;
public Long uptime;
@SerializedName("ram_size")
public Long ramSize;
@SerializedName("ram_free")
public Long ramFree;
@SerializedName("fs_size")
public Long fsSize;
@SerializedName("fs_free")
public Long fsFree;
@SerializedName("cfg_rev")
public Integer cfg_rev;
@SerializedName("available_updates")
public Shelly2DeviceStatusSysAvlUpdate availableUpdates;
@SerializedName("webhook_rev")
public Integer webHookRev;
@SerializedName("wakeup_reason")
public Shelly2DeviceStatusWakeup wakeUpReason;
@SerializedName("wakeup_period")
public Integer wakeupPeriod;
}
public class Shelly2DeviceStatusSysWiFi {
@SerializedName("sta_ip")
public String staIP;
public String status;
public String ssid;
public Integer rssi;
@SerializedName("ap_client_count")
public Integer apClientCount;
}
public String id;
public String src;
public Shelly2DeviceStatusResult result;
}
public static class Shelly2RelayStatus {
public Integer id;
public String source;
public Boolean output;
@SerializedName("timer_started_at")
public Double timerStartetAt;
@SerializedName("timer_duration")
public Integer timerDuration;
public Double apower;
public Double voltage;
public Double current;
public Double pf;
public Shelly2Energy aenergy;
public Shelly2DeviceStatusTemp temperature;
public String[] errors;
}
public static class Shelly2DeviceStatusTemp {
public Double tC;
public Double tF;
}
public static class Shelly2Energy {
// "switch:1":{"id":1,"aenergy":{"total":0.003,"by_minute":[0.000,0.000,0.000],"minute_ts":1619910239}}}}
public Double total;
@SerializedName("by_minute")
public Double[] byMinute;
@SerializedName("minute_ts")
public Long minuteTs;
}
public static class Shelly2ConfigParms {
public String name;
public Boolean enable;
public String server;
@SerializedName("ssl_ca")
public String sslCA;
// WiFi.SetConfig
public Shelly2DeviceConfigAp ap;
// Switch.SetConfig
@SerializedName("auto_on")
public Boolean autoOn;
@SerializedName("auto_on_delay")
public Double autoOnDelay;
@SerializedName("auto_off")
public Boolean autoOff;
@SerializedName("auto_off_delay")
public Double autoOffDelay;
}
public static class Shelly2RpcRequest {
public Integer id = 0;
public String method;
public static class Shelly2RpcRequestParams {
public Integer id = 1;
// Cover
public Integer pos;
public Boolean on;
// Shelly.SetAuth
public String user;
public String realm;
public String ha1;
// Shelly.Update
public String stage;
public String url;
// Cloud.SetConfig
public Shelly2ConfigParms config;
public Shelly2RpcRequestParams withConfig() {
config = new Shelly2ConfigParms();
return this;
}
}
public Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
public Shelly2RpcRequest() {
}
public Shelly2RpcRequest withMethod(String method) {
this.method = method;
return this;
}
public Shelly2RpcRequest withId(int id) {
params.id = id;
return this;
}
public Shelly2RpcRequest withPos(int pos) {
params.pos = pos;
return this;
}
}
public static class Shelly2WsConfigResponse {
public Integer id;
public String src;
public static class Shelly2WsConfigResult {
@SerializedName("restart_required")
public Boolean restartRequired;
}
public Shelly2WsConfigResult result;
}
public static class Shelly2RpcBaseMessage {
// Basic message format, e.g.
// {"id":1,"src":"localweb528","method":"Shelly.GetConfig"}
public class Shelly2RpcMessageError {
public Integer code;
public String message;
}
public Integer id;
public String src;
public String dst;
public String method;
public Object params;
public Object result;
public Shelly2AuthRequest auth;
public Shelly2RpcMessageError error;
}
public static class Shelly2RpcNotifyStatus {
public static class Shelly2NotifyStatus extends Shelly2DeviceStatusResult {
public Double ts;
}
public Integer id;
public String src;
public String dst;
public String method;
public Shelly2NotifyStatus params;
public Shelly2NotifyStatus result;
public Shelly2RpcMessageError error;
}
public static String SHELLY2_AUTHTTYPE_DIGEST = "digest";
public static String SHELLY2_AUTHTTYPE_STRING = "string";
public static String SHELLY2_AUTHALG_SHA256 = "SHA-256";
// = ':auth:'+HexHash("dummy_method:dummy_uri");
public static String SHELLY2_AUTH_NOISE = "6370ec69915103833b5222b368555393393f098bfbfbb59f47e0590af135f062";
public static class Shelly2AuthRequest {
public String username;
public Long nonce;
public Long cnonce;
public Integer nc;
public String realm;
public String algorithm;
public String response;
@SerializedName("auth_type")
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;
}
public class Shelly2NotifyEvent {
public Integer id;
public Double ts;
public String component;
public String event;
public String msg;
public Integer reason;
@SerializedName("cfg_rev")
public Integer cfgRev;
}
public class Shelly2NotifyEventData {
public Double ts;
public ArrayList<Shelly2NotifyEvent> events;
}
public static class Shelly2RpcNotifyEvent {
public Double ts;
Shelly2NotifyEventData params;
}
}

View File

@@ -0,0 +1,931 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api2;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
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.ShellySensorSleepMode;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsWiFiNetwork;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
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.ShellyStatusSensor;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2AuthResponse;
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.Shelly2GetConfigResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceConfigAp.Shelly2DeviceConfigApRE;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceSettings;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusResult;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2DeviceStatus.Shelly2DeviceStatusSys.Shelly2DeviceStatusSysAvlUpdate;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus.Shelly2NotifyStatus;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link Shelly2ApiRpc} implements Gen2 RPC interface
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterface, Shelly2RpctInterface {
private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
private final @Nullable ShellyThingTable thingTable;
private boolean initialized = false;
private boolean discovery = false;
private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
/**
* Regular constructor - called by Thing handler
*
* @param thingName Symbolic thing name
* @param thing Thing Handler (ThingHandlerInterface)
*/
public Shelly2ApiRpc(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
super(thingName, thing);
this.thingName = thingName;
this.thing = thing;
this.thingTable = thingTable;
try {
getProfile().initFromThingType(thing.getThingType());
} catch (ShellyApiException e) {
logger.info("{}: Shelly2 API initialization failed!", thingName, e);
}
}
/**
* Simple initialization - called by discovery handler
*
* @param thingName Symbolic thing name
* @param config Thing Configuration
* @param httpClient HTTP Client to be passed to ShellyHttpClient
*/
public Shelly2ApiRpc(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
super(thingName, config, httpClient);
this.thingName = thingName;
this.thingTable = null;
this.discovery = true;
}
@Override
public void initialize() throws ShellyApiException {
if (!initialized) {
rpcSocket = new Shelly2RpcSocket(thingName, thingTable, config.deviceIp);
rpcSocket.addMessageHandler(this);
initialized = true;
} else {
if (rpcSocket.isConnected()) {
logger.debug("{}: Disconnect Rpc Socket on initialize", thingName);
disconnect();
}
}
}
@Override
public boolean isInitialized() {
return initialized;
}
@Override
public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
Shelly2GetConfigResult dc = apiRequest(SHELLYRPC_METHOD_GETCONFIG, null, Shelly2GetConfigResult.class);
profile.isGen2 = true;
profile.settingsJson = gson.toJson(dc);
profile.thingName = thingName;
profile.settings.name = profile.status.name = dc.sys.device.name;
profile.name = getString(profile.settings.name);
profile.settings.timezone = getString(dc.sys.location.tz);
profile.settings.discoverable = getBool(dc.sys.device.discoverable);
if (dc.wifi != null && dc.wifi.ap != null && dc.wifi.ap.rangeExtender != null) {
profile.settings.wifiAp.rangeExtender = getBool(dc.wifi.ap.rangeExtender.enable);
}
if (dc.cloud != null) {
profile.settings.cloud.enabled = getBool(dc.cloud.enable);
}
if (dc.mqtt != null) {
profile.settings.mqtt.enable = getBool(dc.mqtt.enable);
}
if (dc.sys.sntp != null) {
profile.settings.sntp.server = dc.sys.sntp.server;
}
profile.isRoller = dc.cover0 != null;
profile.settings.relays = fillRelaySettings(profile, dc);
profile.settings.inputs = fillInputSettings(profile, dc);
profile.settings.rollers = fillRollerSettings(profile, dc);
profile.isEMeter = true;
profile.numInputs = profile.settings.inputs != null ? profile.settings.inputs.size() : 0;
profile.numRelays = profile.settings.relays != null ? profile.settings.relays.size() : 0;
profile.numRollers = profile.settings.rollers != null ? profile.settings.rollers.size() : 0;
profile.hasRelays = profile.numRelays > 0 || profile.numRollers > 0;
profile.mode = "";
if (profile.hasRelays) {
profile.mode = profile.isRoller ? SHELLY_CLASS_ROLLER : SHELLY_CLASS_RELAY;
}
ShellySettingsDevice device = getDeviceInfo();
profile.settings.device = device;
profile.hostname = device.hostname;
profile.deviceType = device.type;
profile.mac = device.mac;
profile.auth = device.auth;
if (config.serviceName.isEmpty()) {
config.serviceName = getString(profile.hostname);
}
profile.fwDate = substringBefore(device.fw, "/");
profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
profile.status.update.oldVersion = profile.fwVersion;
profile.status.hasUpdate = profile.status.update.hasUpdate = false;
if (dc.eth != null) {
profile.settings.ethernet = getBool(dc.eth.enable);
}
if (dc.ble != null) {
profile.settings.bluetooth = getBool(dc.ble.enable);
}
profile.settings.wifiSta = new ShellySettingsWiFiNetwork();
profile.settings.wifiSta1 = new ShellySettingsWiFiNetwork();
fillWiFiSta(dc.wifi.sta, profile.settings.wifiSta);
fillWiFiSta(dc.wifi.sta1, profile.settings.wifiSta1);
if (profile.hasRelays) {
profile.status.relays = new ArrayList<>();
profile.status.meters = new ArrayList<>();
profile.status.emeters = new ArrayList<>();
relayStatus.relays = new ArrayList<>();
relayStatus.meters = new ArrayList<>();
profile.numMeters = profile.isRoller ? profile.numRollers : profile.numRelays;
for (int i = 0; i < profile.numRelays; i++) {
profile.status.relays.add(new ShellySettingsRelay());
relayStatus.relays.add(new ShellyShortStatusRelay());
}
for (int i = 0; i < profile.numMeters; i++) {
profile.status.meters.add(new ShellySettingsMeter());
profile.status.emeters.add(new ShellySettingsEMeter());
relayStatus.meters.add(new ShellySettingsMeter());
}
}
if (profile.numInputs > 0) {
profile.status.inputs = new ArrayList<>();
relayStatus.inputs = new ArrayList<>();
for (int i = 0; i < profile.numInputs; i++) {
ShellyInputState input = new ShellyInputState();
input.input = 0;
input.event = "";
input.eventCount = 0;
profile.status.inputs.add(input);
relayStatus.inputs.add(input);
}
}
if (profile.isRoller) {
profile.status.rollers = new ArrayList<>();
for (int i = 0; i < profile.numRollers; i++) {
ShellyRollerStatus rs = new ShellyRollerStatus();
profile.status.rollers.add(rs);
rollerStatus.add(rs);
}
}
profile.status.dimmers = profile.isDimmer ? new ArrayList<>() : null;
profile.status.lights = profile.isBulb ? new ArrayList<>() : null;
profile.status.thermostats = profile.isTRV ? new ArrayList<>() : null;
if (profile.hasBattery) {
profile.settings.sleepMode = new ShellySensorSleepMode();
profile.settings.sleepMode.unit = "m";
profile.settings.sleepMode.period = dc.sys.sleep != null ? dc.sys.sleep.wakeupPeriod / 60 : 720;
checkSetWsCallback();
}
profile.initialized = true;
if (!discovery) {
getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
}
return profile;
}
private void fillWiFiSta(@Nullable Shelly2DeviceConfigSta from, ShellySettingsWiFiNetwork to) {
to.enabled = from != null && !getString(from.ssid).isEmpty();
if (from != null) {
to.ssid = from.ssid;
to.ip = from.ip;
to.mask = from.netmask;
to.dns = from.nameserver;
}
}
private void checkSetWsCallback() throws ShellyApiException {
Shelly2ConfigParms wsConfig = apiRequest(SHELLYRPC_METHOD_WSGETCONFIG, null, Shelly2ConfigParms.class);
String url = "ws://" + config.localIp + ":" + config.localPort + "/shelly/wsevent";
if (!getBool(wsConfig.enable) || !url.equalsIgnoreCase(getString(wsConfig.server))) {
logger.debug("{}: A battery device was detected without correct callback, fix it", thingName);
wsConfig.enable = true;
wsConfig.server = url;
Shelly2RpcRequest request = new Shelly2RpcRequest();
request.id = 0;
request.method = SHELLYRPC_METHOD_WSSETCONFIG;
request.params.config = wsConfig;
Shelly2WsConfigResponse response = apiRequest(SHELLYRPC_METHOD_WSSETCONFIG, request.params,
Shelly2WsConfigResponse.class);
if (response.result != null && response.result.restartRequired) {
logger.info("{}: WebSocket callback was updated, device is restarting", thingName);
getThing().getApi().deviceReboot();
getThing().reinitializeThing();
}
}
}
@Override
public void onConnect(String deviceIp, boolean connected) {
if (thing == null && thingTable != null) {
thing = thingTable.getThing(deviceIp);
logger.debug("{}: Get thing from thingTable", thingName);
}
}
@Override
public void onNotifyStatus(Shelly2RpcNotifyStatus message) {
logger.debug("{}: NotifyStatus update received: {}", thingName, gson.toJson(message));
try {
ShellyThingInterface t = thing;
if (t == null) {
logger.debug("{}: No matching thing on NotifyStatus for {}, ignore (src={}, dst={}, discovery={})",
thingName, thingName, message.src, message.dst, discovery);
return;
}
if (!t.isThingOnline() && t.getThingStatusDetail() != ThingStatusDetail.CONFIGURATION_PENDING) {
logger.debug("{}: Thing is not in online state/connectable, ignore NotifyStatus", thingName);
return;
}
getThing().incProtMessages();
if (message.error != null) {
if (message.error.code == HttpStatus.UNAUTHORIZED_401 && !getString(message.error.message).isEmpty()) {
// Save nonce for notification
Shelly2AuthResponse auth = gson.fromJson(message.error.message, Shelly2AuthResponse.class);
if (auth != null && auth.realm == null) {
logger.debug("{}: Authentication data received: {}", thingName, message.error.message);
authInfo = auth;
}
} else {
logger.debug("{}: Error status received - {} {}", thingName, message.error.code,
message.error.message);
incProtErrors();
}
}
Shelly2NotifyStatus params = message.params;
if (params != null) {
if (getThing().getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
getThing().setThingOnline();
}
boolean updated = false;
ShellyDeviceProfile profile = getProfile();
ShellySettingsStatus status = profile.status;
if (params.sys != null) {
if (getBool(params.sys.restartRequired)) {
logger.warn("{}: Device requires restart to activate changes", thingName);
}
status.uptime = params.sys.uptime;
}
status.temperature = SHELLY_API_INVTEMP; // mark invalid
updated |= fillDeviceStatus(status, message.params, true);
if (getDouble(status.temperature) == SHELLY_API_INVTEMP) {
// no device temp available
status.temperature = null;
} else {
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
}
profile.status = status;
if (updated) {
getThing().restartWatchdog();
}
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process status update", thingName, e);
incProtErrors();
}
}
@Override
public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
try {
logger.debug("{}: NotifyEvent received: {}", thingName, gson.toJson(message));
ShellyDeviceProfile profile = getProfile();
getThing().incProtMessages();
getThing().restartWatchdog();
for (Shelly2NotifyEvent e : message.params.events) {
switch (e.event) {
case SHELLY2_EVENT_BTNUP:
case SHELLY2_EVENT_BTNDOWN:
String bgroup = getProfile().getInputGroup(e.id);
updateChannel(bgroup, CHANNEL_INPUT + profile.getInputSuffix(e.id),
getOnOff(SHELLY2_EVENT_BTNDOWN.equals(getString(e.event))));
getThing().triggerButton(profile.getInputGroup(e.id), e.id,
mapValue(MAP_INPUT_EVENT_ID, e.event));
break;
case SHELLY2_EVENT_1PUSH:
case SHELLY2_EVENT_2PUSH:
case SHELLY2_EVENT_3PUSH:
case SHELLY2_EVENT_LPUSH:
case SHELLY2_EVENT_SLPUSH:
case SHELLY2_EVENT_LSPUSH:
if (e.id < profile.numInputs) {
ShellyInputState input = relayStatus.inputs.get(e.id);
input.event = getString(MAP_INPUT_EVENT_TYPE.get(e.event));
input.eventCount = getInteger(input.eventCount) + 1;
relayStatus.inputs.set(e.id, input);
profile.status.inputs.set(e.id, input);
String group = getProfile().getInputGroup(e.id);
updateChannel(group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(e.id),
getStringType(input.event));
updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(e.id),
getDecimal(input.eventCount));
getThing().triggerButton(profile.getInputGroup(e.id), e.id,
mapValue(MAP_INPUT_EVENT_ID, e.event));
}
break;
case SHELLY2_EVENT_CFGCHANGED:
logger.debug("{}: Configuration update detected, re-initialize", thingName);
getThing().requestUpdates(1, true); // refresh config
break;
case SHELLY2_EVENT_OTASTART:
logger.debug("{}: Firmware update started: {}", thingName, getString(e.msg));
getThing().postEvent(e.event, true);
getThing().setThingOffline(ThingStatusDetail.FIRMWARE_UPDATING,
"offline.status-error-fwupgrade");
break;
case SHELLY2_EVENT_OTAPROGRESS:
logger.debug("{}: Firmware update in progress: {}", thingName, getString(e.msg));
getThing().postEvent(e.event, false);
break;
case SHELLY2_EVENT_OTADONE:
logger.debug("{}: Firmware update completed: {}", thingName, getString(e.msg));
getThing().setThingOffline(ThingStatusDetail.CONFIGURATION_PENDING,
"offline.status-error-restarted");
getThing().requestUpdates(1, true); // refresh config
break;
case SHELLY2_EVENT_SLEEP:
logger.debug("{}: Device went to sleep mode", thingName);
break;
case SHELLY2_EVENT_WIFICONNFAILED:
logger.debug("{}: WiFi connect failed, check setup, reason {}", thingName,
getInteger(e.reason));
getThing().postEvent(e.event, false);
break;
case SHELLY2_EVENT_WIFIDISCONNECTED:
logger.debug("{}: WiFi disconnected, reason {}", thingName, getInteger(e.reason));
getThing().postEvent(e.event, false);
break;
default:
logger.debug("{}: Event {} was not handled", thingName, e.event);
}
}
} catch (ShellyApiException e) {
logger.debug("{}: Unable to process event", thingName, e);
incProtErrors();
}
}
@Override
public void onMessage(String message) {
logger.debug("{}: Unexpected RPC message received: {}", thingName, message);
incProtErrors();
}
@Override
public void onClose(int statusCode, String reason) {
try {
logger.debug("{}: WebSocket connection closed, status = {}/{}", thingName, statusCode, getString(reason));
if (statusCode == StatusCode.ABNORMAL && !discovery && getProfile().alwaysOn) { // e.g. device rebooted
thingOffline("WebSocket connection closed abnormal");
}
} catch (ShellyApiException e) {
logger.debug("{}: Exception on onClose()", thingName, e);
incProtErrors();
}
}
@Override
public void onError(Throwable cause) {
logger.debug("{}: WebSocket error", thingName);
if (thing != null && thing.getProfile().alwaysOn) {
thingOffline("WebSocket error");
}
}
private void thingOffline(String reason) {
if (thing != null) { // do not reinit of battery powered devices with sleep mode
thing.setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-unexpected-error",
reason);
}
}
@Override
public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
Shelly2DeviceSettings device = callApi("/shelly", Shelly2DeviceSettings.class);
ShellySettingsDevice info = new ShellySettingsDevice();
info.hostname = getString(device.id);
info.fw = getString(device.firmware);
info.type = getString(device.model);
info.mac = getString(device.mac);
info.auth = getBool(device.authEnable);
info.gen = getInteger(device.gen);
return info;
}
@Override
public ShellySettingsStatus getStatus() throws ShellyApiException {
ShellyDeviceProfile profile = getProfile();
ShellySettingsStatus status = profile.status;
Shelly2DeviceStatusResult ds = apiRequest(SHELLYRPC_METHOD_GETSTATUS, null, Shelly2DeviceStatusResult.class);
status.time = ds.sys.time;
status.uptime = ds.sys.uptime;
status.cloud.connected = getBool(ds.cloud.connected);
status.mqtt.connected = getBool(ds.mqtt.connected);
status.wifiSta.ssid = getString(ds.wifi.ssid);
status.wifiSta.enabled = !status.wifiSta.ssid.isEmpty();
status.wifiSta.ip = getString(ds.wifi.staIP);
status.wifiSta.rssi = getInteger(ds.wifi.rssi);
status.fsFree = ds.sys.fsFree;
status.fsSize = ds.sys.fsSize;
status.discoverable = getBool(profile.settings.discoverable);
if (ds.sys.wakeupPeriod != null) {
profile.settings.sleepMode.period = ds.sys.wakeupPeriod / 60;
}
status.hasUpdate = status.update.hasUpdate = false;
status.update.oldVersion = getProfile().fwVersion;
if (ds.sys.availableUpdates != null) {
status.update.hasUpdate = ds.sys.availableUpdates.stable != null;
if (ds.sys.availableUpdates.stable != null) {
status.update.newVersion = "v" + getString(ds.sys.availableUpdates.stable.version);
}
if (ds.sys.availableUpdates.beta != null) {
status.update.betaVersion = "v" + getString(ds.sys.availableUpdates.beta.version);
}
}
if (ds.sys.wakeUpReason != null && ds.sys.wakeUpReason.boot != null) {
List<Object> values = new ArrayList<>();
String boot = getString(ds.sys.wakeUpReason.boot);
String cause = getString(ds.sys.wakeUpReason.cause);
// Index 0 is aggregated status, 1 boot, 2 cause
String reason = boot.equals(SHELLY2_WAKEUPO_BOOT_RESTART) ? ALARM_TYPE_RESTARTED : cause;
values.add(reason);
values.add(ds.sys.wakeUpReason.boot);
values.add(ds.sys.wakeUpReason.cause);
getThing().updateWakeupReason(values);
}
fillDeviceStatus(status, ds, false);
return status;
}
@Override
public void setSleepTime(int value) throws ShellyApiException {
}
@Override
public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
if (getProfile().status.wifiSta.ssid == null) {
// Update status when not yet initialized
getStatus();
}
return relayStatus;
}
@Override
public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
params.id = id;
params.on = SHELLY_API_ON.equals(turnMode);
apiRequest(SHELLYRPC_METHOD_SWITCH_SET, params, String.class);
}
@Override
public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
if (rollerIndex < rollerStatus.size()) {
return rollerStatus.get(rollerIndex);
}
throw new IllegalArgumentException("Invalid rollerIndex on getRollerStatus");
}
@Override
public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
String operation = "";
switch (turnMode) {
case SHELLY_ALWD_ROLLER_TURN_OPEN:
operation = SHELLY2_COVER_CMD_OPEN;
break;
case SHELLY_ALWD_ROLLER_TURN_CLOSE:
operation = SHELLY2_COVER_CMD_CLOSE;
break;
case SHELLY_ALWD_ROLLER_TURN_STOP:
operation = SHELLY2_COVER_CMD_STOP;
break;
}
apiRequest(new Shelly2RpcRequest().withMethod("Cover." + operation).withId(relayIndex));
}
@Override
public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
apiRequest(
new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_COVER_SETPOS).withId(relayIndex).withPos(position));
}
@Override
public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
return sensorData;
}
@Override
public void setAutoTimer(int index, String timerName, double value) throws ShellyApiException {
Shelly2RpcRequest req = new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SWITCH_SETCONFIG).withId(index);
req.params.withConfig();
req.params.config.name = "Switch" + index;
if (timerName.equals(SHELLY_TIMER_AUTOON)) {
req.params.config.autoOn = value > 0;
req.params.config.autoOnDelay = value;
} else {
req.params.config.autoOff = value > 0;
req.params.config.autoOffDelay = value;
}
apiRequest(req);
}
@Override
public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
return new ShellySettingsLogin();
}
@Override
public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
params.user = "admin";
params.realm = config.serviceName;
params.ha1 = sha256(params.user + ":" + params.realm + ":" + password);
apiRequest(SHELLYRPC_METHOD_AUTHSET, params, String.class);
ShellySettingsLogin res = new ShellySettingsLogin();
res.enabled = true;
res.username = params.user;
res.password = password;
return new ShellySettingsLogin();
}
@Override
public boolean setWiFiRangeExtender(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.ap = new Shelly2DeviceConfigAp();
params.config.ap.rangeExtender = new Shelly2DeviceConfigApRE();
params.config.ap.rangeExtender.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_WIFISETCONG, params, Shelly2WsConfigResult.class);
return res.restartRequired;
}
@Override
public boolean setEthernet(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_ETHSETCONG, params, Shelly2WsConfigResult.class);
return res.restartRequired;
}
@Override
public boolean setBluetooth(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_BLESETCONG, params, Shelly2WsConfigResult.class);
return res.restartRequired;
}
@Override
public String deviceReboot() throws ShellyApiException {
return apiRequest(SHELLYRPC_METHOD_REBOOT, null, String.class);
}
@Override
public String factoryReset() throws ShellyApiException {
return apiRequest(SHELLYRPC_METHOD_RESET, null, String.class);
}
@Override
public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
Shelly2DeviceStatusSysAvlUpdate status = apiRequest(SHELLYRPC_METHOD_CHECKUPD, null,
Shelly2DeviceStatusSysAvlUpdate.class);
ShellyOtaCheckResult result = new ShellyOtaCheckResult();
result.status = status.stable != null || status.beta != null ? "new" : "ok";
return result;
}
@Override
public ShellySettingsUpdate firmwareUpdate(String fwurl) throws ShellyApiException {
ShellySettingsUpdate res = new ShellySettingsUpdate();
boolean prod = fwurl.contains("update");
boolean beta = fwurl.contains("beta");
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams();
if (prod || beta) {
params.stage = prod || beta ? "stable" : "beta";
} else {
params.url = fwurl;
}
apiRequest(SHELLYRPC_METHOD_UPDATE, params, String.class);
res.status = "Update initiated";
return res;
}
@Override
public String setCloud(boolean enable) throws ShellyApiException {
Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
params.config.enable = enable;
Shelly2WsConfigResult res = apiRequest(SHELLYRPC_METHOD_CLOUDSET, params, Shelly2WsConfigResult.class);
return res.restartRequired ? "restart required" : "ok";
}
@Override
public String setDebug(boolean enabled) throws ShellyApiException {
return "failed";
}
@Override
public String getDebugLog(String id) throws ShellyApiException {
return ""; // Gen2 uses WS to publish debug log
}
/*
* The following API calls are not yet relevant, because currently there a no Plus/Pro (Gen2) devices of those
* categories (e.g. bulbs)
*/
@Override
public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public ShellyStatusLight getLightStatus() throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setLightMode(String mode) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValvePosition(int valveId, double value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveTemperature(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveProfile(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void startValveBoost(int valveId, int value) throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public String resetStaCache() throws ShellyApiException {
throw new ShellyApiException("API call not implemented");
}
@Override
public void setActionURLs() throws ShellyApiException {
// not relevant for Gen2
}
@Override
public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
// not relevant for Gen2
return new ShellySettingsLogin();
}
@Override
public String getCoIoTDescription() {
return ""; // not relevant to Gen2
}
@Override
public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
throw new ShellyApiException("API call not implemented");
}
@Override
public String setWiFiRecovery(boolean enable) throws ShellyApiException {
return "failed"; // not supported by Gen2
}
@Override
public String setApRoaming(boolean enable) throws ShellyApiException {
return "false";// not supported by Gen2
}
private void asyncApiRequest(String method) throws ShellyApiException {
Shelly2RpcBaseMessage request = buildRequest(method, null);
reconnect();
rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
}
public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
String json = "";
Shelly2RpcBaseMessage req = buildRequest(method, params);
try {
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));
} catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult();
String auth = getString(res.authResponse);
if (res.isHttpAccessUnauthorized() && !auth.isEmpty()) {
String[] options = auth.split(",");
for (String o : options) {
String key = substringBefore(o, "=").stripLeading().trim();
String value = substringAfter(o, "=").replaceAll("\"", "").trim();
switch (key) {
case "Digest qop":
break;
case "realm":
authInfo.realm = value;
break;
case "nonce":
authInfo.nonce = Long.parseLong(value, 16);
break;
case "algorithm":
authInfo.algorithm = value;
break;
}
}
authInfo.nc = 1;
req.auth = buildAuthRequest(authInfo, config.userId, authInfo.realm, config.password);
json = rpcPost(gson.toJson(req));
} else {
throw e;
}
}
json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
return fromJson(gson, json, classOfT);
}
public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
return apiRequest(request.method, request.params, classOfT);
}
public String apiRequest(Shelly2RpcRequest request) throws ShellyApiException {
return apiRequest(request.method, request.params, String.class);
}
private String rpcPost(String postData) throws ShellyApiException {
return httpPost("/rpc", postData);
}
private void reconnect() throws ShellyApiException {
if (!rpcSocket.isConnected()) {
logger.debug("{}: Connect Rpc Socket (discovery = {})", thingName, discovery);
rpcSocket.connect();
}
}
private void disconnect() {
if (rpcSocket.isConnected()) {
rpcSocket.disconnect();
}
}
public Shelly2RpctInterface getRpcHandler() {
return this;
}
@Override
public void close() {
logger.debug("{}: Closing Rpc API (socket is {}, discovery={})", thingName,
rpcSocket.isConnected() ? "connected" : "disconnected", discovery);
disconnect();
initialized = false;
}
private void incProtErrors() {
if (thing != null) {
thing.incProtErrors();
}
}
}

View File

@@ -0,0 +1,311 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api2;
import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import javax.ws.rs.core.HttpHeaders;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
* (not
* cloud api).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
@WebSocket(maxIdleTime = Integer.MAX_VALUE)
public class Shelly2RpcSocket {
private final Logger logger = LoggerFactory.getLogger(Shelly2RpcSocket.class);
private final Gson gson = new Gson();
private String thingName = "";
private String deviceIp = "";
private boolean inbound = false;
private CountDownLatch connectLatch = new CountDownLatch(1);
private @Nullable Session session;
private @Nullable Shelly2RpctInterface websocketHandler;
private WebSocketClient client = new WebSocketClient();
private @Nullable ShellyThingTable thingTable;
public Shelly2RpcSocket() {
}
/**
* Regular constructor for Thing and Discover handler
*
* @param thingName Thing/Service name
* @param thingTable
* @param deviceIp IP address for the device
*/
public Shelly2RpcSocket(String thingName, @Nullable ShellyThingTable thingTable, String deviceIp) {
this.thingName = thingName;
this.deviceIp = deviceIp;
this.thingTable = thingTable;
}
/**
* Constructor called from Servlet handler
*
* @param thingTable
* @param inbound
*/
public Shelly2RpcSocket(ShellyThingTable thingTable, boolean inbound) {
this.thingTable = thingTable;
this.inbound = inbound;
}
/**
* Add listener for inbound messages implementing Shelly2RpctInterface
*
* @param interfacehandler
*/
public void addMessageHandler(Shelly2RpctInterface interfacehandler) {
this.websocketHandler = interfacehandler;
}
/**
* Connect outbound Web Socket
*
* @throws ShellyApiException
*/
public void connect() throws ShellyApiException {
try {
disconnect(); // for safety
URI uri = new URI("ws://" + deviceIp + "/rpc");
ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setHeader(HttpHeaders.HOST, deviceIp);
request.setHeader("Origin", "http://" + deviceIp);
request.setHeader("Pragma", "no-cache");
request.setHeader("Cache-Control", "no-cache");
logger.debug("{}: Connect WebSocket, URI={}", thingName, uri);
client = new WebSocketClient();
connectLatch = new CountDownLatch(1);
client.start();
client.setConnectTimeout(5000);
client.setStopTimeout(0);
client.connect(this, uri, request);
} catch (Exception e) {
throw new ShellyApiException("Unable to initialize WebSocket", e);
}
}
/**
* Web Socket is connected, lookup thing and create connectLatch to synchronize first sendMessage()
*
* @param session Newly created WebSocket connection
*/
@OnWebSocketConnect
public void onConnect(Session session) {
try {
if (session.getRemoteAddress() == null) {
logger.debug("{}: Invalid inbound WebSocket connect", thingName);
session.close(StatusCode.ABNORMAL, "Invalid remote IP");
return;
}
this.session = session;
if (deviceIp.isEmpty()) {
// This is the inbound event web socket
deviceIp = session.getRemoteAddress().getAddress().getHostAddress();
}
if (websocketHandler == null) {
if (thingTable != null) {
ShellyThingInterface thing = thingTable.getThing(deviceIp);
Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
websocketHandler = api.getRpcHandler();
}
}
connectLatch.countDown();
logger.debug("{}: WebSocket connected {}<-{}, Idle Timeout={}", thingName, session.getLocalAddress(),
session.getRemoteAddress(), session.getIdleTimeout());
if (websocketHandler != null) {
websocketHandler.onConnect(deviceIp, true);
return;
}
} catch (IllegalArgumentException e) { // unknown thing
// debug is below
}
if (websocketHandler == null && thingTable != null) {
logger.debug("Rpc: Unable to handle connection from {} (unknown/disabled thing), closing socket", deviceIp);
session.close(StatusCode.SHUTDOWN, "Thing not active");
}
}
/**
* Send request over WebSocket
*
* @param str API request message
* @throws ShellyApiException
*/
@SuppressWarnings("null")
public void sendMessage(String str) throws ShellyApiException {
if (session != null) {
try {
connectLatch.await();
session.getRemote().sendString(str);
return;
} catch (IOException | InterruptedException e) {
throw new ShellyApiException("Error RpcSend failed", e);
}
}
throw new ShellyApiException("Unable to send API request (No Rpc session)");
}
/**
* Close WebSocket session
*/
public void disconnect() {
try {
if (session != null) {
Session s = session;
if (s.isOpen()) {
logger.debug("{}: Disconnecting WebSocket ({} -> {})", thingName, s.getLocalAddress(),
s.getRemoteAddress());
s.disconnect();
}
s.close(StatusCode.NORMAL, "Socket closed");
session = null;
}
if (client.isStarted()) {
client.stop();
}
} catch (Exception e) {
if (e.getCause() instanceof InterruptedException) {
logger.debug("{}: Unable to close socket - interrupted", thingName); // e.g. device was rebooted
} else {
logger.debug("{}: Unable to close socket", thingName, e);
}
}
}
/**
* Inbound WebSocket message
*
* @param session WebSpcket session
* @param receivedMessage Textial API message
*/
@OnWebSocketMessage
public void onText(Session session, String receivedMessage) {
try {
Shelly2RpctInterface handler = websocketHandler;
Shelly2RpcBaseMessage message = fromJson(gson, receivedMessage, Shelly2RpcBaseMessage.class);
logger.trace("{}: Inbound Rpc message: {}", thingName, receivedMessage);
if (handler != null) {
if (thingName.isEmpty()) {
thingName = getString(message.src);
}
if (message.method == null) {
message.method = SHELLYRPC_METHOD_NOTIFYFULLSTATUS;
}
switch (getString(message.method)) {
case SHELLYRPC_METHOD_NOTIFYSTATUS:
case SHELLYRPC_METHOD_NOTIFYFULLSTATUS:
Shelly2RpcNotifyStatus status = fromJson(gson, receivedMessage, Shelly2RpcNotifyStatus.class);
if (status.params == null) {
status.params = status.result;
}
handler.onNotifyStatus(status);
return;
case SHELLYRPC_METHOD_NOTIFYEVENT:
handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
return;
default:
handler.onMessage(receivedMessage);
}
} else {
logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
getString(message.src), receivedMessage);
}
} catch (ShellyApiException | IllegalArgumentException | NullPointerException e) {
logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
}
}
public boolean isConnected() {
return session != null && session.isOpen();
}
public boolean isInbound() {
return inbound;
}
/**
* Web Socket closed, notify thing handler
*
* @param statusCode StatusCode
* @param reason Textual reason
*/
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
if (statusCode != StatusCode.NORMAL) {
logger.trace("{}: Rpc connection closed: {} - {}", thingName, statusCode, getString(reason));
}
if (inbound) {
// Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
return;
}
disconnect();
if (websocketHandler != null) {
websocketHandler.onClose(statusCode, reason);
}
}
/**
* WebSocket error handler
*
* @param cause WebSocket error/Exception
*/
@OnWebSocketError
public void onError(Throwable cause) {
if (inbound) {
// Ignore disconnect: Device establishes the socket, sends NotifyxFullStatus and disconnects
return;
}
if (websocketHandler != null) {
websocketHandler.onError(cause);
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.shelly.internal.api2;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
/**
* The {@link WebsocketInterface} is responsible for interfacing the Websocket.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public interface Shelly2RpctInterface {
public void onConnect(String deviceIp, boolean connected);
public void onMessage(String decodedmessage);
public void onNotifyStatus(Shelly2RpcNotifyStatus message);
public void onNotifyEvent(Shelly2RpcNotifyEvent message);
public void onClose(int statusCode, String reason);
public void onError(Throwable cause);
}

View File

@@ -27,9 +27,11 @@ 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;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
@@ -139,15 +141,20 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
config.userId = bindingConfig.defaultUserId;
config.password = bindingConfig.defaultPassword;
boolean gen2 = "2".equals(service.getPropertyString("gen"));
try {
Shelly1HttpApi api = new Shelly1HttpApi(name, config, httpClient);
ShellyApiInterface api = gen2 ? new Shelly2ApiRpc(name, config, httpClient)
: new Shelly1HttpApi(name, config, httpClient);
if (name.contains("plushat")) {
int i = 1;
}
api.initialize();
profile = api.getDeviceProfile(thingType);
api.close();
logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
deviceName = profile.name;
model = profile.deviceType;
mode = profile.mode;
properties = ShellyBaseHandler.fillDeviceProperties(profile);
logger.trace("{}: thingType={}, deviceType={}, mode={}, symbolic name={}", name, thingType,
profile.deviceType, mode.isEmpty() ? "<standard>" : mode, deviceName);
@@ -174,7 +181,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
addProperty(properties, PROPERTY_SERVICE_NAME, name);
addProperty(properties, PROPERTY_DEV_NAME, deviceName);
addProperty(properties, PROPERTY_DEV_TYPE, thingType);
addProperty(properties, PROPERTY_DEV_GEN, "1");
addProperty(properties, PROPERTY_DEV_GEN, gen2 ? "2" : "1");
addProperty(properties, PROPERTY_DEV_MODE, mode);
logger.debug("{}: Adding Shelly {}, UID={}", name, deviceName, thingUID.getAsString());

View File

@@ -69,6 +69,8 @@ public class ShellyThingCreator {
public static final String SHELLYDT_PLUS1PMUL = "SNSW-001P15UL";
public static final String SHELLYDT_PLUS2PM_RELAY = "SNSW-002P16EU-relay";
public static final String SHELLYDT_PLUS2PM_ROLLER = "SNSW-002P16EU-roller";
public static final String SHELLYDT_PLUS2PM_RELAY_2 = "SNSW-102P16EU-relay";
public static final String SHELLYDT_PLUS2PM_ROLLER_2 = "SNSW-102P16EU-roller";
public static final String SHELLYDT_PLUSPLUGUS = "SNPL-00116US";
public static final String SHELLYDT_PLUSI4 = "SNSN-0024X";
public static final String SHELLYDT_PLUSI4DC = "SNSN-0D24X";
@@ -77,17 +79,19 @@ public class ShellyThingCreator {
// Shelly Pro Series
public static final String SHELLYDT_PRO1 = "SPSW-001XE16EU";
public static final String SHELLYDT_PRO1_2 = "SPSW-101XE16EU";
public static final String SHELLYDT_PRO1_3 = "SPSW-201XE16EU";
public static final String SHELLYDT_PRO1PM = "SPSW-001PE16EU";
public static final String SHELLYDT_PRO1PM_ = "SPSW-201PE16EU";
public static final String SHELLYDT_PRO1PM_2 = "SPSW-101PE16EU";
public static final String SHELLYDT_PRO1PM_3 = "SPSW-201PE16EU";
public static final String SHELLYDT_PRO2_RELAY = "SPSW-002XE16EU-relay";
public static final String SHELLYDT_PRO2_ROLLER = "SPSW-002XE16EU-roller";
public static final String SHELLYDT_PRO2_RELAY_2 = "SPSW-102XE16EU-relay";
public static final String SHELLYDT_PRO2_ROLLER_2 = "SPSW-102XE16EU-roller";
public static final String SHELLYDT_PRO2_RELAY_3 = "SPSW-202XE16EU-relay";
public static final String SHELLYDT_PRO2PM_RELAY = "SPSW-002PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER = "SPSW-002PE16EU-roller";
public static final String SHELLYDT_PRO2PM_RELAY_2 = "SPSW-002PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER_2 = "SPSW-002PE16EU-roller";
public static final String SHELLYDT_PRO2PM_RELAY_2 = "SPSW-102PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER_2 = "SPSW-102PE16EU-roller";
public static final String SHELLYDT_PRO2PM_RELAY_3 = "SPSW-202PE16EU-relay";
public static final String SHELLYDT_PRO2PM_ROLLER_3 = "SPSW-202PE16EU-roller";
public static final String SHELLYDT_PRO3 = "SPSW-003XE16EU";
public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU";
@@ -100,7 +104,6 @@ public class ShellyThingCreator {
public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
@@ -147,7 +150,6 @@ public class ShellyThingCreator {
public static final String THING_TYPE_SHELLYPRO1_STR = "shellypro1";
public static final String THING_TYPE_SHELLYPRO1PM_STR = "shellypro1pm";
public static final String THING_TYPE_SHELLYPRO2_RELAY_STR = "shellypro2-relay";
public static final String THING_TYPE_SHELLYPRO2_ROLLER_STR = "shellypro2-roller";
public static final String THING_TYPE_SHELLYPRO2PM_RELAY_STR = "shellypro2pm-relay";
public static final String THING_TYPE_SHELLYPRO2PM_ROLLER_STR = "shellypro2pm-roller";
public static final String THING_TYPE_SHELLYPRO3_STR = "shellypro3";
@@ -164,8 +166,6 @@ public class ShellyThingCreator {
public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY2_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLY25_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
@@ -233,8 +233,6 @@ public class ShellyThingCreator {
THING_TYPE_SHELLYPRO1PM_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO2_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2_ROLLER = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO2_ROLLER_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_RELAY = new ThingTypeUID(BINDING_ID,
THING_TYPE_SHELLYPRO2PM_RELAY_STR);
public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_ROLLER = new ThingTypeUID(BINDING_ID,
@@ -277,6 +275,8 @@ public class ShellyThingCreator {
THING_TYPE_MAPPING.put(SHELLYDT_PLUS1PMUL, THING_TYPE_SHELLYPLUS1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_RELAY, THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_ROLLER, THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_RELAY_2, THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_ROLLER_2, THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSPLUGUS, THING_TYPE_SHELLYPLUSPLUGUS_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4DC, THING_TYPE_SHELLYPLUSI4DC_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4, THING_TYPE_SHELLYPLUSI4_STR);
@@ -285,16 +285,19 @@ public class ShellyThingCreator {
// Pro Series
THING_TYPE_MAPPING.put(SHELLYDT_PRO1, THING_TYPE_SHELLYPRO1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1_2, THING_TYPE_SHELLYPRO1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1_3, THING_TYPE_SHELLYPRO1_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM, THING_TYPE_SHELLYPRO1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM_2, THING_TYPE_SHELLYPRO1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM_3, THING_TYPE_SHELLYPRO1PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY, THING_TYPE_SHELLYPRO2_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY_2, THING_TYPE_SHELLYPRO2_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER, THING_TYPE_SHELLYPRO2_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER_2, THING_TYPE_SHELLYPRO2_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY_3, THING_TYPE_SHELLYPRO2_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY_2, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY_3, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER_2, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER_3, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO3, THING_TYPE_SHELLYPRO3_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM, THING_TYPE_SHELLYPRO4PM_STR);
THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, THING_TYPE_SHELLYPRO4PM_STR);
@@ -364,7 +367,7 @@ public class ShellyThingCreator {
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY25_RELAY_STR : THING_TYPE_SHELLY25_ROLLER_STR;
}
if (name.startsWith(THING_TYPE_SHELLY2_PREFIX)) { // Shelly v2
return mode.equals(SHELLY_MODE_RELAY) ? THING_TYPE_SHELLY2_RELAY_STR : THING_TYPE_SHELLY2_ROLLER_STR;
return THING_TYPE_SHELLY2_RELAY_STR;
}
if (name.startsWith(THING_TYPE_SHELLYPLUG_STR)) {
// shellyplug-s needs to be mapped to shellyplugs to follow the schema

View File

@@ -21,13 +21,9 @@ import static org.openhab.core.thing.Thing.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -39,19 +35,21 @@ import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyApiResult;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyFavPos;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapHandler;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
import org.openhab.binding.shelly.internal.provider.ShellyStateDescriptionProvider;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyChannelCache;
import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
@@ -66,7 +64,6 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
@@ -84,23 +81,11 @@ import org.slf4j.LoggerFactory;
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyBaseHandler extends BaseThingHandler
implements ShellyDeviceListener, ShellyManagerInterface, ShellyThingInterface {
private class OptionEntry {
public ChannelTypeUID uid;
public String key;
public String value;
public OptionEntry(ChannelTypeUID uid, String key, String value) {
this.uid = uid;
this.key = key;
this.value = value;
}
}
public abstract class ShellyBaseHandler extends BaseThingHandler
implements ShellyThingInterface, ShellyDeviceListener, ShellyManagerInterface {
protected final Logger logger = LoggerFactory.getLogger(ShellyBaseHandler.class);
protected final ShellyChannelDefinitions channelDefinitions;
private final CopyOnWriteArrayList<OptionEntry> stateOptions = new CopyOnWriteArrayList<>();
public String thingName = "";
public String thingType = "";
@@ -111,28 +96,29 @@ public class ShellyBaseHandler extends BaseThingHandler
private ShellyBindingConfiguration bindingConfig;
protected ShellyThingConfiguration config = new ShellyThingConfiguration();
protected ShellyDeviceProfile profile = new ShellyDeviceProfile(); // init empty profile to avoid NPE
protected ShellyDeviceStats stats = new ShellyDeviceStats();
private final Shelly1CoapHandler coap;
public boolean autoCoIoT = false;
private ShellyDeviceStats stats = new ShellyDeviceStats();
private @Nullable Shelly1CoapHandler coap;
private final ShellyTranslationProvider messages;
protected boolean stopping = false;
private final ShellyChannelCache cache;
private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final boolean gen2;
protected boolean autoCoIoT = false;
// Thing status
private boolean channelsCreated = false;
private boolean stopping = false;
private int vibrationFilter = 0;
private String lastWakeupReason = "";
// Scheduler
private long watchdog = now();
private @Nullable ScheduledFuture<?> statusJob;
public int scheduledUpdates = 0;
protected int scheduledUpdates = 0;
private int skipCount = UPDATE_SKIP_COUNT;
private int skipUpdate = 0;
private boolean refreshSettings = false;
// delay before enabling channel
private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
private final ShellyChannelCache cache;
private String lastWakeupReason = "";
private int vibrationFilter = 0;
private @Nullable ScheduledFuture<?> statusJob;
/**
* Constructor
@@ -155,26 +141,28 @@ public class ShellyBaseHandler extends BaseThingHandler
this.channelDefinitions = new ShellyChannelDefinitions(messages);
this.bindingConfig = bindingConfig;
this.config = getConfigAs(ShellyThingConfiguration.class);
this.httpClient = httpClient;
this.api = new Shelly1HttpApi(thingName, config, httpClient);
coap = new Shelly1CoapHandler(this, coapServer);
Map<String, String> properties = thing.getProperties();
String gen = getString(properties.get(PROPERTY_DEV_GEN));
String thingType = getThingType();
if (gen.isEmpty() && thingType.startsWith("shellyplus") || thingType.startsWith("shellypro")) {
gen = "2";
}
gen2 = "2".equals(gen);
this.api = !gen2 ? new Shelly1HttpApi(thingName, this) : new Shelly2ApiRpc(thingName, thingTable, this);
if (gen2) {
config.eventsCoIoT = false;
}
if (config.eventsCoIoT) {
this.coap = new Shelly1CoapHandler(this, coapServer);
}
}
@Override
public boolean checkRepresentation(String key) {
return key.equalsIgnoreCase(getUID()) || key.equalsIgnoreCase(config.deviceIp)
|| key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(thing.getUID().getAsString());
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(ShellyStateDescriptionProvider.class);
}
public String getUID() {
return getThing().getUID().getAsString();
|| key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(getThingName());
}
/**
@@ -199,6 +187,10 @@ public class ShellyBaseHandler extends BaseThingHandler
start = initializeThing();
} catch (ShellyApiException e) {
ShellyApiResult res = e.getApiResult();
if (profile.alwaysOn && e.isConnectionError()) {
setThingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "offline.status-error-connect",
e.toString());
}
if (isAuthorizationFailed(res)) {
start = false;
}
@@ -255,22 +247,25 @@ public class ShellyBaseHandler extends BaseThingHandler
lastWakeupReason = "";
cache.setThingName(thingName);
cache.clear();
resetStats();
logger.debug("{}: Start initializing thing {}, type {}, ip address {}, CoIoT: {}", thingName,
getThing().getLabel(), thingType, config.deviceIp, config.eventsCoIoT);
logger.debug("{}: Start initializing for thing {}, type {}, IP address {}, Gen2: {}, CoIoT: {}", thingName,
getThing().getLabel(), thingType, config.deviceIp, gen2, config.eventsCoIoT);
if (config.deviceIp.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-ip");
return false;
}
// Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing could not be
// fully initialized here. In this case the CoAP messages triggers auto-initialization (like the Action URL does
// when enabled)
if (config.eventsCoIoT && !profile.alwaysOn) {
// Gen 1 only: Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing
// could not be fully initialized here. In this case the CoAP messages triggers auto-initialization (like the
// Action URL does when enabled)
if (coap != null && config.eventsCoIoT && !profile.alwaysOn) {
coap.start(thingName, config);
}
// Initialize API access, exceptions will be catched by initialize()
api.initialize();
profile.initFromThingType(thingType);
ShellySettingsDevice devInfo = api.getDeviceInfo();
if (getBool(devInfo.auth) && config.password.isEmpty()) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-no-credentials");
@@ -281,8 +276,10 @@ public class ShellyBaseHandler extends BaseThingHandler
}
api.setConfig(thingName, config);
api.initialize();
ShellyDeviceProfile tmpPrf = api.getDeviceProfile(thingType);
tmpPrf.isGen2 = gen2;
tmpPrf.auth = devInfo.auth; // missing in /settings
if (this.getThing().getThingTypeUID().equals(THING_TYPE_SHELLYPROTECTED)) {
changeThingType(thingName, tmpPrf.mode);
return false; // force re-initialization
@@ -290,7 +287,8 @@ public class ShellyBaseHandler extends BaseThingHandler
// Validate device mode
String reqMode = thingType.contains("-") ? substringAfter(thingType, "-") : "";
if (!reqMode.isEmpty() && !tmpPrf.mode.equals(reqMode)) {
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode");
setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode", tmpPrf.mode,
reqMode);
return false;
}
if (!getString(devInfo.coiot).isEmpty()) {
@@ -311,82 +309,32 @@ public class ShellyBaseHandler extends BaseThingHandler
tmpPrf.updatePeriod = UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
}
tmpPrf.auth = devInfo.auth; // missing in /settings
tmpPrf.status = api.getStatus();
tmpPrf.status = api.getStatus(); // update thing properties
tmpPrf.updateFromStatus(tmpPrf.status);
addStateOptions(tmpPrf);
if (tmpPrf.isTRV) {
String[] profileNames = tmpPrf.getValveProfileList(0);
String channelId = mkChannelId(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE);
logger.debug("{}: Adding TRV profile names to channel description: {}", thingName, profileNames);
clearStateOptions(channelId);
addStateOption(channelId, "0", "DISABLED");
for (int i = 0; i < profileNames.length; i++) {
addStateOption(channelId, "" + (i + 1), profileNames[i]);
}
}
showThingConfig(tmpPrf);
// update thing properties
updateProperties(tmpPrf, tmpPrf.status);
checkVersion(tmpPrf, tmpPrf.status);
if (config.eventsCoIoT && (tmpPrf.settings.coiot != null) && (tmpPrf.settings.coiot.enabled != null)) {
String devpeer = getString(tmpPrf.settings.coiot.peer);
String ourpeer = config.localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT;
if (!tmpPrf.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
try {
api.setCoIoTPeer(ourpeer);
logger.info("{}: CoIoT peer updated to {}", thingName, ourpeer);
} catch (ShellyApiException e) {
logger.warn("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
}
} else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) {
logger.warn("{}: CoIoT peer in device settings does not point this to this host", thingName);
}
}
if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true;
config.eventsSwitch = false;
config.eventsButton = false;
config.eventsPush = false;
config.eventsRoller = false;
config.eventsSensorReport = false;
api.setConfig(thingName, config);
startCoap(config, tmpPrf);
if (!gen2) {
api.setActionURLs(); // register event urls
}
// All initialization done, so keep the profile and set Thing to ONLINE
fillDeviceStatus(tmpPrf.status, false);
postEvent(ALARM_TYPE_NONE, false);
api.setActionURLs(); // register event urls
if (config.eventsCoIoT) {
logger.debug("{}: Starting CoIoT (autoCoIoT={}/{})", thingName, bindingConfig.autoCoIoT, autoCoIoT);
coap.start(thingName, config);
}
profile = tmpPrf;
showThingConfig(profile);
logger.debug("{}: Thing successfully initialized.", thingName);
profile = tmpPrf;
updateProperties(tmpPrf, tmpPrf.status);
updateProperties(profile, profile.status);
setThingOnline(); // if API call was successful the thing must be online
return true; // success
}
private void showThingConfig(ShellyDeviceProfile profile) {
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
profile.hostname, profile.deviceType, profile.hwRev, profile.hwBatchId, profile.fwVersion,
profile.fwDate);
logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
+ ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter, profile.isSensor,
profile.isDW, profile.hasBattery,
profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", profile.isSense,
profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2, profile.inColor,
profile.alwaysOn, profile.updatePeriod);
}
/**
* Handle Channel Commands
*/
@@ -443,17 +391,31 @@ public class ShellyBaseHandler extends BaseThingHandler
break;
case CHANNEL_CONTROL_PROFILE:
logger.debug("{}: Select profile {}", thingName, command);
String cmd = command.toString();
int id = Integer.parseInt(cmd);
int id = -1;
if (command instanceof Number) {
id = (int) getNumber(command);
} else {
String cmd = command.toString();
if (isDigit(cmd.charAt(0))) {
id = Integer.parseInt(cmd);
} else if (profile.settings.thermostats != null) {
ShellyThermnostat t = profile.settings.thermostats.get(0);
for (int i = 0; i < t.profileNames.length; i++) {
if (t.profileNames[i].equalsIgnoreCase(cmd)) {
id = i + 1;
}
}
}
}
if (id < 0 || id > 5) {
logger.warn("{}: Invalid profile Id {} requested", thingName, profile);
} else {
api.setValveProfile(0, id);
break;
}
api.setValveProfile(0, id);
break;
case CHANNEL_CONTROL_MODE:
logger.debug("{}: Set mode to {}", thingName, command);
api.setValveMode(0, SHELLY_TRV_MODE_AUTO.equalsIgnoreCase(command.toString()));
api.setValveMode(0, CHANNEL_CONTROL_MODE.equalsIgnoreCase(command.toString()));
break;
case CHANNEL_CONTROL_SETTEMP:
logger.debug("{}: Set temperature to {}", thingName, command);
@@ -519,7 +481,7 @@ public class ShellyBaseHandler extends BaseThingHandler
if (vibrationFilter > 0) {
vibrationFilter--;
logger.debug("{}: Vibration events are absorbed for {} more seconds", thingName,
logger.debug("{}: Vibration events are absorbed for {} more seconds", thingName,
vibrationFilter * UPDATE_STATUS_INTERVAL_SECONDS);
}
@@ -533,6 +495,9 @@ public class ShellyBaseHandler extends BaseThingHandler
}
// Get profile, if refreshSettings == true reload settings from device
ShellySettingsStatus status = api.getStatus();
if (status.uptime != null && status.uptime == 0 && profile.alwaysOn) {
status = api.getStatus();
}
boolean restarted = checkRestarted(status);
profile = getProfile(refreshSettings || restarted);
profile.status = status;
@@ -563,7 +528,11 @@ public class ShellyBaseHandler extends BaseThingHandler
// sleep mode. Once the next update is successful the device goes back online
String status = "";
ShellyApiResult res = e.getApiResult();
if (isWatchdogStarted()) {
if (profile.alwaysOn && e.isConnectionError()) {
status = "offline.status-error-connect";
} else if (res.isHttpAccessUnauthorized()) {
status = "offline.conf-error-access-denied";
} else if (isWatchdogStarted()) {
if (!isWatchdogExpired()) {
logger.debug("{}: Ignore API Timeout, retry later", thingName);
} else {
@@ -571,8 +540,6 @@ public class ShellyBaseHandler extends BaseThingHandler
status = "offline.status-error-watchdog";
}
}
} else if (res.isHttpAccessUnauthorized()) {
status = "offline.conf-error-access-denied";
} else if (e.isJSONException()) {
status = "offline.status-error-unexpected-api-result";
logger.debug("{}: Unable to parse API response: {}; json={}", thingName, res.getUrl(), res.response, e);
@@ -603,31 +570,73 @@ public class ShellyBaseHandler extends BaseThingHandler
}
}
private void showThingConfig(ShellyDeviceProfile profile) {
logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {}", thingName,
profile.hostname, profile.deviceType, profile.hwRev, profile.hwBatchId, profile.fwVersion,
profile.fwDate);
logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
logger.debug("{}: Device "
+ "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})"
+ ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+ ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter, profile.isSensor,
profile.isDW, profile.hasBattery,
profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", profile.isSense,
profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2, profile.inColor,
profile.alwaysOn, profile.updatePeriod);
}
private void addStateOptions(ShellyDeviceProfile prf) {
if (prf.isTRV) {
String[] profileNames = prf.getValveProfileList(0);
String channelId = mkChannelId(CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE);
logger.debug("{}: Adding TRV profile names to channel description: {}", thingName, profileNames);
channelDefinitions.clearStateOptions(channelId);
int fid = 1;
for (String name : profileNames) {
channelDefinitions.addStateOption(channelId, "" + fid, fid + ": " + name);
fid++;
}
}
if (prf.isRoller && prf.settings.favorites != null) {
String channelId = mkChannelId(CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_FAV);
logger.debug("{}: Adding {} roler favorite(s) to channel description", thingName,
prf.settings.favorites.size());
channelDefinitions.clearStateOptions(channelId);
int fid = 1;
for (ShellyFavPos fav : prf.settings.favorites) {
channelDefinitions.addStateOption(channelId, "" + fid, fid + ": " + fav.name);
fid++;
}
}
}
@Override
public String getThingType() {
return thing.getThingTypeUID().getId();
}
@Override
public ThingStatus getThingStatus() {
return getThing().getStatus();
return thing.getStatus();
}
@Override
public ThingStatusDetail getThingStatusDetail() {
return getThing().getStatusInfo().getStatusDetail();
return thing.getStatusInfo().getStatusDetail();
}
@Override
public boolean isThingOnline() {
return getThing().getStatus() == ThingStatus.ONLINE;
return getThingStatus() == ThingStatus.ONLINE;
}
public boolean isThingOffline() {
return getThing().getStatus() == ThingStatus.OFFLINE;
return getThingStatus() == ThingStatus.OFFLINE;
}
@Override
public void setThingOnline() {
if (stopping) {
logger.debug("{}: Thing should go ONLINE, but handler is shutting down, ignore!", thingName);
return;
}
if (!isThingOnline()) {
updateStatus(ThingStatus.ONLINE);
@@ -640,16 +649,10 @@ public class ShellyBaseHandler extends BaseThingHandler
}
@Override
public void setThingOffline(ThingStatusDetail detail, String messageKey) {
String message = messages.get(messageKey);
if (stopping) {
logger.debug("{}: Thing should go OFFLINE with status {}, but handler is shutting down -> ignore",
thingName, message);
return;
}
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments) {
if (!isThingOffline()) {
updateStatus(ThingStatus.OFFLINE, detail, message);
updateStatus(ThingStatus.OFFLINE, detail, messages.get(messageKey, arguments));
api.close(); // Gen2: disconnect WS/close http sessions
watchdog = 0;
channelsCreated = false; // check for new channels after devices gets re-initialized (e.g. new
}
@@ -723,15 +726,22 @@ public class ShellyBaseHandler extends BaseThingHandler
if (status.uptime != null) {
stats.lastUptime = getLong(status.uptime);
}
if (coap != null) {
stats.coiotMessages = coap.getMessageCount();
stats.coiotErrors = coap.getErrorCount();
}
if (!alarm.isEmpty()) {
postEvent(alarm, false);
}
}
@Override
public void incProtMessages() {
stats.protocolMessages++;
}
@Override
public void incProtErrors() {
stats.protocolErrors++;
}
/**
* Check if device has restarted and needs a new Thing initialization
*
@@ -741,8 +751,10 @@ public class ShellyBaseHandler extends BaseThingHandler
private boolean checkRestarted(ShellySettingsStatus status) {
if (profile.isInitialized() && profile.alwaysOn /* exclude battery powered devices */
&& (status.uptime != null && status.uptime < stats.lastUptime
|| !profile.status.update.oldVersion.isEmpty()
&& !status.update.oldVersion.equals(profile.status.update.oldVersion))) {
|| (!profile.status.update.oldVersion.isEmpty()
&& !status.update.oldVersion.equals(profile.status.update.oldVersion)))) {
logger.debug("{}: Device has been restarted, uptime={}/{}, firmware={}/{}", thingName, stats.lastUptime,
getLong(status.uptime), profile.status.update.oldVersion, status.update.oldVersion);
updateProperties(profile, status);
return true;
}
@@ -785,6 +797,10 @@ public class ShellyBaseHandler extends BaseThingHandler
}
}
public boolean isUpdateScheduled() {
return scheduledUpdates > 0;
}
/**
* Callback for device events
*
@@ -980,12 +996,13 @@ public class ShellyBaseHandler extends BaseThingHandler
if (version.checkBeta(getString(prf.fwVersion))) {
logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate));
} else {
if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) {
String minVersion = !gen2 ? SHELLY_API_MIN_FWVERSION : SHELLY2_API_MIN_FWVERSION;
if (version.compare(prf.fwVersion, minVersion) < 0) {
logger.warn("{}: {}", prf.hostname,
messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, SHELLY_API_MIN_FWVERSION));
messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, minVersion));
}
}
if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
if (!gen2 && bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
|| (prf.fwVersion.equalsIgnoreCase("production_test"))) {
if (!config.eventsCoIoT) {
logger.info("{}: {}", thingName, messages.get("versioncheck.autocoiot"));
@@ -1001,6 +1018,50 @@ public class ShellyBaseHandler extends BaseThingHandler
}
}
public String checkForUpdate() {
try {
ShellyOtaCheckResult result = api.checkForUpdate();
return result.status;
} catch (ShellyApiException e) {
return "";
}
}
public void startCoap(ShellyThingConfiguration config, ShellyDeviceProfile profile) throws ShellyApiException {
if (coap == null || !config.eventsCoIoT) {
return;
}
if (profile.settings.coiot != null && profile.settings.coiot.enabled != null) {
String devpeer = getString(profile.settings.coiot.peer);
String ourpeer = config.localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT;
if (!profile.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
try {
api.setCoIoTPeer(ourpeer);
logger.info("{}: CoIoT peer updated to {}", thingName, ourpeer);
} catch (ShellyApiException e) {
logger.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString());
}
} else if (!devpeer.isEmpty() && !devpeer.equals(ourpeer)) {
logger.warn("{}: CoIoT peer in device settings does not point this to this host", thingName);
}
}
if (autoCoIoT) {
logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
config.eventsCoIoT = true;
config.eventsSwitch = false;
config.eventsButton = false;
config.eventsPush = false;
config.eventsRoller = false;
config.eventsSensorReport = false;
api.setConfig(thingName, config);
}
logger.debug("{}: Starting CoIoT (autoCoIoT={}/{})", thingName, bindingConfig.autoCoIoT, autoCoIoT);
if (coap != null) {
coap.start(thingName, config);
}
}
/**
* Checks the http response for authorization error.
* If the authorization failed the binding can't access the device settings and determine the thing type. In this
@@ -1081,10 +1142,6 @@ public class ShellyBaseHandler extends BaseThingHandler
return false;
}
public boolean isUpdateScheduled() {
return scheduledUpdates > 0;
}
/**
* Map input states to channels
*
@@ -1098,17 +1155,15 @@ public class ShellyBaseHandler extends BaseThingHandler
boolean updated = false;
if (status.inputs != null) {
if (!areChannelsCreated()) {
updateChannelDefinitions(ShellyChannelDefinitions.createInputChannels(thing, profile, status));
}
int idx = 0;
boolean multiInput = status.inputs.size() >= 2; // device has multiple SW (inputs)
boolean multiInput = !profile.isIX && status.inputs.size() >= 2; // device has multiple SW (inputs)
for (ShellyInputState input : status.inputs) {
String group = profile.getInputGroup(idx);
String suffix = multiInput ? profile.getInputSuffix(idx) : "";
if (!areChannelsCreated()) {
updateChannelDefinitions(
ShellyChannelDefinitions.createInputChannels(thing, profile, status, group));
}
updated |= updateChannel(group, CHANNEL_INPUT + suffix, getOnOff(input.input));
if (input.event != null) {
updated |= updateChannel(group, CHANNEL_STATUS_EVENTTYPE + suffix, getStringType(input.event));
@@ -1248,12 +1303,14 @@ public class ShellyBaseHandler extends BaseThingHandler
*/
public void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) {
Map<String, Object> properties = fillDeviceProperties(profile);
properties.put(PROPERTY_SERVICE_NAME, config.serviceName);
String deviceName = getString(profile.settings.name);
properties.put(PROPERTY_SERVICE_NAME, config.serviceName);
properties.put(PROPERTY_DEV_GEN, "1");
if (!deviceName.isEmpty()) {
properties.put(PROPERTY_DEV_NAME, deviceName);
}
properties.put(PROPERTY_DEV_GEN, !profile.isGen2 ? "1" : "2");
// add status properties
if (status.wifiSta != null) {
@@ -1372,32 +1429,13 @@ public class ShellyBaseHandler extends BaseThingHandler
}
@Override
public List<StateOption> getStateOptions(ChannelTypeUID uid) {
List<StateOption> options = new ArrayList<>();
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
options.add(new StateOption(oe.key, oe.value));
}
}
public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid) {
List<StateOption> options = channelDefinitions.getStateOptions(uid);
if (!options.isEmpty()) {
logger.debug("{}: Return {} state options for channel uid {}", thingName, options.size(), uid.getId());
return options;
}
return options;
}
private void addStateOption(String channelId, String key, String value) {
ChannelTypeUID uid = channelDefinitions.getChannelTypeUID(channelId);
stateOptions.addIfAbsent(new OptionEntry(uid, key, value));
}
private void clearStateOptions(String channelId) {
ChannelTypeUID uid = channelDefinitions.getChannelTypeUID(channelId);
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
stateOptions.remove(oe);
}
}
return null;
}
protected ShellyDeviceProfile getDeviceProfile() {
@@ -1431,10 +1469,7 @@ public class ShellyBaseHandler extends BaseThingHandler
statusJob = null;
logger.debug("{}: Shelly statusJob stopped", thingName);
}
if (coap != null) {
coap.stop();
}
api.close();
profile.initialized = false;
}
@@ -1443,6 +1478,7 @@ public class ShellyBaseHandler extends BaseThingHandler
*/
@Override
public void dispose() {
logger.debug("{}: Stopping Thing", thingName);
stopping = true;
stop();
super.dispose();
@@ -1455,6 +1491,10 @@ public class ShellyBaseHandler extends BaseThingHandler
return false;
}
public String getUID() {
return getThing().getUID().getAsString();
}
/**
* Device specific handlers are overriding this method to do additional stuff
*/
@@ -1483,22 +1523,13 @@ public class ShellyBaseHandler extends BaseThingHandler
return api;
}
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
@Override
public long getScheduledUpdates() {
return scheduledUpdates;
}
public String checkForUpdate() {
try {
ShellyOtaCheckResult result = api.checkForUpdate();
return result.status;
} catch (ShellyApiException e) {
return "";
}
public Map<String, String> getStatsProp() {
return stats.asProperties();
}
@Override

View File

@@ -33,8 +33,8 @@ public class ShellyDeviceStats {
public long alarms = 0;
public String lastAlarm = "";
public long lastAlarmTs = 0;
public long coiotMessages = 0;
public long coiotErrors = 0;
public long protocolMessages = 0;
public long protocolErrors = 0;
public int wifiRssi = 0;
public int maxInternalTemp = 0;
@@ -48,8 +48,8 @@ public class ShellyDeviceStats {
prop.put("alarmCount", String.valueOf(alarms));
prop.put("lastAlarm", lastAlarm);
prop.put("lastAlarmTs", ShellyUtils.convertTimestamp(lastAlarmTs));
prop.put("coiotMessages", String.valueOf(coiotMessages));
prop.put("coiotErrors", String.valueOf(coiotErrors));
prop.put("protocolMessages", String.valueOf(protocolMessages));
prop.put("protocolErrors", String.valueOf(protocolErrors));
prop.put("wifiRssi", String.valueOf(wifiRssi));
return prop;
}

View File

@@ -46,7 +46,11 @@ public interface ShellyManagerInterface {
public void setThingOnline();
public void setThingOffline(ThingStatusDetail detail, String messageKey);
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
public boolean requestUpdates(int requestCount, boolean refreshSettings);
public void incProtMessages();
public void incProtErrors();
}

View File

@@ -111,7 +111,9 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
channelUID.getIdWithoutGroup().equals(CHANNEL_ROL_CONTROL_CONTROL));
// request updates the next 45sec to update roller position after it stopped
requestUpdates(autoCoIoT ? 1 : 45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
if (!autoCoIoT && !profile.isGen2) {
requestUpdates(45 / UPDATE_STATUS_INTERVAL_SECONDS, false);
}
break;
case CHANNEL_ROL_CONTROL_FAV:
@@ -129,11 +131,11 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
case CHANNEL_TIMER_AUTOON:
logger.debug("{}: Set Auto-ON timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).intValue());
api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).doubleValue());
break;
case CHANNEL_TIMER_AUTOOFF:
logger.debug("{}: Set Auto-OFF timer to {}", thingName, command);
api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue());
api.setAutoTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).doubleValue());
break;
}
return true;
@@ -241,7 +243,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
position = shpos;
} else {
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
position = SHELLY_MIN_ROLLER_POS;
}
} else if (command == UpDownType.DOWN || command == OnOffType.OFF
|| ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) {
@@ -255,7 +256,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
position = shpos;
} else {
api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE);
position = SHELLY_MAX_ROLLER_POS;
}
}
} else if (command == StopMoveType.STOP) {
@@ -283,18 +283,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
logger.debug("{}: Changing roller position to {}", thingName, position);
api.setRollerPos(index, position);
}
if (position != -1) {
// make sure both are in sync
if (isControl) {
int pos = SHELLY_MAX_ROLLER_POS - Math.max(0, Math.min(position, SHELLY_MAX_ROLLER_POS));
logger.debug("{}: Set roller position for control channel to {}", thingName, pos);
updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, new PercentType(pos));
} else {
logger.debug("{}: Set roller position channel to {}", thingName, position);
updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, new PercentType(position));
}
}
}
/**
@@ -329,7 +317,6 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
*/
public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiException {
boolean updated = false;
if (profile.hasRelays && !profile.isDimmer) {
double voltage = -1;
if (status.voltage == null && profile.settings.supplyVoltage != null) {
@@ -343,25 +330,23 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
updated |= updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_VOLTAGE,
toQuantityType(voltage, DIGITS_VOLT, Units.VOLT));
}
}
if (profile.hasRelays && !profile.isRoller) {
logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
for (int i = 0; i < status.relays.size(); i++) {
createRelayChannels(status.relays.get(i), i);
updated |= ShellyComponents.updateRelay(this, status, i);
i++;
}
} else {
// Check for Relay in Roller Mode
logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
for (int i = 0; i < profile.numRollers; i++) {
ShellyRollerStatus roller = status.rollers.get(i);
createRollerChannels(roller);
updated |= ShellyComponents.updateRoller(this, roller, i);
if (!profile.isRoller) {
logger.trace("{}: Updating {} relay(s)", thingName, profile.numRelays);
for (int i = 0; i < status.relays.size(); i++) {
createRelayChannels(status.relays.get(i), i);
updated |= ShellyComponents.updateRelay(this, status, i);
}
} else {
// Check for Relay in Roller Mode
logger.trace("{}: Updating {} rollers", thingName, profile.numRollers);
for (int i = 0; i < profile.numRollers; i++) {
ShellyRollerStatus roller = status.rollers.get(i);
createRollerChannels(roller);
updated |= ShellyComponents.updateRoller(this, roller, i);
}
}
}
return updated;
}

View File

@@ -41,7 +41,7 @@ public interface ShellyThingInterface {
public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
public List<StateOption> getStateOptions(ChannelTypeUID uid);
public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid);
public double getChannelDouble(String group, String channel);
@@ -51,7 +51,9 @@ public interface ShellyThingInterface {
public void setThingOnline();
public void setThingOffline(ThingStatusDetail detail, String messageKey);
public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
public String getThingType();
public ThingStatus getThingStatus();
@@ -110,4 +112,8 @@ public interface ShellyThingInterface {
public void fillDeviceStatus(ShellySettingsStatus status, boolean updated);
public boolean checkRepresentation(String key);
public void incProtMessages();
public void incProtErrors();
}

View File

@@ -31,6 +31,9 @@ public class ShellyThingTable {
private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
public void addThing(String key, ShellyThingInterface thing) {
if (thingTable.containsKey(key)) {
thingTable.remove(key);
}
thingTable.put(key, thing);
}

View File

@@ -138,7 +138,7 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
}
String peer = getString(profile.settings.coiot.peer);
boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer);
boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer) || profile.isMotion;
String newPeer = mcast ? localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
String displayPeer = mcast ? newPeer : "Multicast";
@@ -252,7 +252,6 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
refreshTimer = 3;
}
break;
case ACTION_ENAPROAMING:
case ACTION_DISAPROAMING:
enable = ACTION_ENAPROAMING.equalsIgnoreCase(action);
@@ -269,6 +268,77 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
refreshTimer = 3;
}
break;
case ACTION_ENRANGEEXT:
case ACTION_DISRANGEEXT:
enable = ACTION_ENRANGEEXT.equalsIgnoreCase(action);
if (!"yes".equalsIgnoreCase(update)) {
message = getMessage(
enable ? "action.setwifirangeext-enable" : "action.setwifirangeext-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
boolean res = api.setWiFiRangeExtender(enable);
if (res) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
actionUrl = buildActionUrl(uid, ACTION_RESTART);
} else {
message = getMessage("action.setwifirangeext-confirm", res ? "yes" : "no");
refreshTimer = 5;
}
} catch (ShellyApiException e) {
message = getMessage("action.setwifirangeext-failed", e.toString());
refreshTimer = 10;
}
}
break;
case ACTION_ENETHERNET:
case ACTION_DISETHERNET:
enable = ACTION_ENETHERNET.equalsIgnoreCase(action);
if (!"yes".equalsIgnoreCase(update)) {
message = getMessage(enable ? "action.setethernet-enable" : "action.setethernet-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
boolean res = api.setEthernet(enable);
if (res) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
actionUrl = buildActionUrl(uid, ACTION_RESTART);
} else {
message = getMessage("action.setethernet-confirm", res ? "yes" : "no");
refreshTimer = 5;
}
} catch (ShellyApiException e) {
message = getMessage("action.setethernet-failed", e.toString());
refreshTimer = 10;
}
}
break;
case ACTION_ENBLUETOOTH:
case ACTION_DISBLUETOOTH:
enable = ACTION_ENBLUETOOTH.equalsIgnoreCase(action);
if (!"yes".equalsIgnoreCase(update)) {
message = getMessage(enable ? "action.setbluetooth-enable" : "action.setbluetooth-disable");
actionUrl = buildActionUrl(uid, action);
} else {
try {
boolean res = api.setBluetooth(enable);
if (res) {
message = getMessageP("action.restart.info", MCINFO);
actionButtonLabel = "Ok";
actionUrl = buildActionUrl(uid, ACTION_RESTART);
} else {
message = getMessage("action.setbluetooth-confirm", res ? "yes" : "no");
refreshTimer = 5;
}
} catch (ShellyApiException e) {
message = getMessage("action.setbluetooth-failed", e.toString());
refreshTimer = 10;
}
}
break;
case ACTION_GETDEB:
case ACTION_GETDEB1:
@@ -303,35 +373,51 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
public static Map<String, String> getActions(ShellyDeviceProfile profile) {
Map<String, String> list = new LinkedHashMap<>();
boolean gen2 = profile.isGen2;
list.put(ACTION_RES_STATS, "Reset Statistics");
list.put(ACTION_RESTART, "Reboot Device");
list.put(ACTION_PROTECT, "Protect Device");
if (gen2) {
list.put(ACTION_PROTECT, "Protect Device");
}
if ((profile.settings.coiot != null) && (profile.settings.coiot.peer != null) && !profile.isMotion) {
if ((profile.settings.coiot != null) && profile.settings.coiot.peer != null) {
boolean mcast = profile.settings.coiot.peer.isEmpty()
|| SHELLY_COIOT_MCAST.equalsIgnoreCase(profile.settings.coiot.peer);
|| SHELLY_COIOT_MCAST.equalsIgnoreCase(profile.settings.coiot.peer) || profile.isMotion;
list.put(mcast ? ACTION_SETCOIOT_PEER : ACTION_SETCOIOT_MCAST,
mcast ? "Set CoIoT Peer Mode" : "Set CoIoT Multicast Mode");
}
if (profile.isSensor && !profile.isMotion && (profile.settings.wifiSta != null)
if (profile.isSensor && !profile.isMotion && profile.settings.wifiSta != null
&& profile.settings.wifiSta.enabled) {
// FW 1.10+: Reset STA list, force WiFi rescan and connect to stringest AP
list.put(ACTION_RESSTA, "Reconnect WiFi");
}
if (profile.settings.apRoaming != null) {
if (!gen2 && profile.settings.apRoaming != null) {
list.put(!profile.settings.apRoaming.enabled ? ACTION_ENAPROAMING : ACTION_DISAPROAMING,
!profile.settings.apRoaming.enabled ? "Enable WiFi Roaming" : "Disable WiFi Roaming");
}
if (profile.settings.wifiRecoveryReboot != null) {
if (!gen2 && profile.settings.wifiRecoveryReboot != null) {
list.put(!profile.settings.wifiRecoveryReboot ? ACTION_ENWIFIREC : ACTION_DISWIFIREC,
!profile.settings.wifiRecoveryReboot ? "Enable WiFi Recovery" : "Disable WiFi Recovery");
}
if (profile.settings.wifiAp != null && profile.settings.wifiAp.rangeExtender != null) {
list.put(!profile.settings.wifiAp.rangeExtender ? ACTION_ENRANGEEXT : ACTION_DISRANGEEXT,
!profile.settings.wifiAp.rangeExtender ? "Enable Range Extender" : "Disable Range Extender");
}
if (profile.settings.ethernet != null) {
list.put(!profile.settings.ethernet ? ACTION_ENETHERNET : ACTION_DISETHERNET,
!profile.settings.ethernet ? "Enable Ethernet" : "Disable Ethernet");
}
if (profile.settings.bluetooth != null) {
list.put(!profile.settings.bluetooth ? ACTION_ENBLUETOOTH : ACTION_DISBLUETOOTH,
!profile.settings.bluetooth ? "Enable Bluetooth" : "Disable Bluetooth");
}
boolean set = profile.settings.cloud != null && profile.settings.cloud.enabled;
list.put(set ? ACTION_DISCLOUD : ACTION_ENCLOUD, set ? "Disable Cloud" : "Enable Cloud");
list.put(ACTION_RESET, "-Factory Reset");
if (profile.extFeatures) {
if (!gen2 && profile.extFeatures) {
list.put(ACTION_OTACHECK, "Check for Update");
boolean debug_enable = getBool(profile.settings.debugEnable);
list.put(!debug_enable ? ACTION_ENDEBUG : ACTION_DISDEBUG,

View File

@@ -39,7 +39,6 @@ public class ShellyManagerConstants {
public static final String ACTION_SETCOIOT_PEER = "setcoiotpeer";
public static final String ACTION_SETCOIOT_MCAST = "setcoiotmcast";
public static final String ACTION_SETTZ = "settz";
public static final String ACTION_SETNTP = "setntp";
public static final String ACTION_ENCLOUD = "encloud";
public static final String ACTION_DISCLOUD = "discloud";
public static final String ACTION_RES_STATS = "reset_stat";
@@ -49,6 +48,12 @@ public class ShellyManagerConstants {
public static final String ACTION_DISWIFIREC = "diswifirec";
public static final String ACTION_ENAPROAMING = "enaproaming";
public static final String ACTION_DISAPROAMING = "disaproaming";
public static final String ACTION_ENRANGEEXT = "enrangeext";
public static final String ACTION_ENETHERNET = "enethernet";
public static final String ACTION_DISETHERNET = "disethernet";
public static final String ACTION_ENBLUETOOTH = "enbluetooth";
public static final String ACTION_DISBLUETOOTH = "disbluetooth";
public static final String ACTION_DISRANGEEXT = "disrangeext";
public static final String ACTION_OTACHECK = "otacheck";
public static final String ACTION_ENDEBUG = "endebug";
public static final String ACTION_DISDEBUG = "disdebug";

View File

@@ -33,9 +33,9 @@ import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
import org.openhab.binding.shelly.internal.api.ShellyApiException;
import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -122,7 +122,7 @@ public class ShellyManagerOtaPage extends ShellyManagerPage {
new Thread(() -> { // schedule asynchronous reboot
try {
Shelly1HttpApi api = new Shelly1HttpApi(uid, config, httpClient);
ShellyApiInterface api = th.getApi();
ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
String status = getString(result.status);
logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));

View File

@@ -111,7 +111,7 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
properties.put(ATTRIBUTE_STATUS_ICON, ICON_ATTENTION);
}
if (!"unknown".equalsIgnoreCase(deviceType) && (status == ThingStatus.ONLINE)) {
properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(uid, deviceType, profile.mode));
properties.put(ATTRIBUTE_FIRMWARE_SEL, fillFirmwareHtml(profile, uid, deviceType));
properties.put(ATTRIBUTE_ACTION_LIST, fillActionHtml(th, uid));
} else {
properties.put(ATTRIBUTE_FIRMWARE_SEL, "");
@@ -132,7 +132,8 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
return new ShellyMgrResponse(fillAttributes(html, properties), HttpStatus.OK_200);
}
private String fillFirmwareHtml(String uid, String deviceType, String mode) throws ShellyApiException {
private String fillFirmwareHtml(ShellyDeviceProfile profile, String uid, String deviceType)
throws ShellyApiException {
String html = "\n\t\t\t\t<select name=\"fwList\" id=\"fwList\" onchange=\"location = this.options[this.selectedIndex].value;\">\n";
html += "\t\t\t\t\t<option value=\"\" selected disabled hidden>update to</option>\n";
@@ -140,45 +141,55 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
String bVersion = "";
String updateUrl = SHELLY_MGR_FWUPDATE_URI + "?" + URLPARM_UID + "=" + urlEncode(uid);
try {
// Get current prod + beta version from original firmware repo
logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
FwRepoEntry fw = getFirmwareRepoEntry(deviceType, mode);
pVersion = extractFwVersion(fw.version);
if (!profile.isGen2) { // currently there is no public firmware repo for Gen2
logger.debug("{}: Load firmware version list for device type {}", LOG_PREFIX, deviceType);
FwRepoEntry fw = getFirmwareRepoEntry(deviceType, profile.mode);
pVersion = extractFwVersion(fw.version);
bVersion = extractFwVersion(fw.betaVer);
} else {
pVersion = extractFwVersion(getString(profile.status.update.newVersion));
bVersion = extractFwVersion(getString(profile.status.update.betaVersion));
}
if (!pVersion.isEmpty()) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWPROD + "\">Release "
+ pVersion + "</option>\n";
}
bVersion = extractFwVersion(fw.betaVer);
if (!bVersion.isEmpty()) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + FWBETA + "\">Beta "
+ bVersion + "</option>\n";
}
// Add those from Shelly Firmware Archive
String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
if (json.startsWith("[]")) {
// no files available for this device type
logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
} else {
// Create selection list
json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
FwArchList list = getFirmwareArchiveList(deviceType);
ArrayList<FwArchEntry> versions = list.versions;
if (versions != null) {
html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
for (int i = versions.size() - 1; i >= 0; i--) {
FwArchEntry e = versions.get(i);
String version = getString(e.version);
ShellyVersionDTO v = new ShellyVersionDTO();
if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
&& (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0) || version.contains("master")) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
+ "\">" + version + "</option>\n";
if (!profile.isGen2) { // currently no online repo for Gen2
// Add those from Shelly Firmware Archive
String json = httpGet(FWREPO_ARCH_URL + "?" + URLPARM_TYPE + "=" + deviceType);
if (json.startsWith("[]")) {
// no files available for this device type
logger.debug("{}: No firmware files found for device type {}", LOG_PREFIX, deviceType);
} else {
// Create selection list
json = "{" + json.replace("[{", "\"versions\":[{") + "}"; // make it an named array
FwArchList list = getFirmwareArchiveList(deviceType);
ArrayList<FwArchEntry> versions = list.versions;
if (versions != null) {
html += "\t\t\t\t\t<option value=\"\" disabled>-- Archive:</option>\n";
for (int i = versions.size() - 1; i >= 0; i--) {
FwArchEntry e = versions.get(i);
String version = getString(e.version);
ShellyVersionDTO v = new ShellyVersionDTO();
if (!version.equalsIgnoreCase(pVersion) && !version.equalsIgnoreCase(bVersion)
&& (v.compare(version, SHELLY_API_MIN_FWCOIOT) >= 0)
|| version.contains("master")) {
html += "\t\t\t\t\t<option value=\"" + updateUrl + "&" + URLPARM_VERSION + "=" + version
+ "\">" + version + "</option>\n";
}
}
}
}
}
} catch (ShellyApiException e) {
} catch (
ShellyApiException e) {
logger.debug("{}: Unable to retrieve firmware list: {}", LOG_PREFIX, e.toString());
}
@@ -282,9 +293,9 @@ public class ShellyManagerOverviewPage extends ShellyManagerPage {
if ((config.eventsCoIoT) && (profile.settings.coiot != null)) {
if ((profile.settings.coiot.enabled != null) && !profile.settings.coiot.enabled) {
result.put("CoIoT Status", "COIOT_DISABLED");
} else if (stats.coiotMessages == 0) {
} else if (stats.protocolMessages == 0) {
result.put("CoIoT Discovery", "NO_COIOT_DISCOVERY");
} else if (stats.coiotMessages < 2) {
} else if (stats.protocolMessages < 2) {
result.put("CoIoT Multicast", "NO_COIOT_MULTICAST");
}
}

View File

@@ -261,6 +261,9 @@ public class ShellyManagerPage {
properties.put(ATTRIBUTE_ACTIONS_SKIPPED,
profile.status.astats != null ? String.valueOf(profile.status.astats.skipped) : "n/a");
properties.put(ATTRIBUTE_MAX_ITEMP, stats.maxInternalTemp > 0 ? stats.maxInternalTemp + " °C" : "n/a");
if (stats.maxInternalTemp == 0) {
properties.replace(CHANNEL_DEVST_ITEMP, "n/a");
}
// Shelly H&T: When external power is connected the battery level is not valid
if (!profile.isHT || (getInteger(profile.settings.externalPower) == 0)) {

View File

@@ -16,12 +16,15 @@ import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_API_INVTEMP;
import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.measure.Unit;
@@ -47,6 +50,7 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -97,6 +101,20 @@ public class ShellyChannelDefinitions {
public static final String PREFIX_GROUP = "group-type." + BINDING_ID + ".";
public static final String PREFIX_CHANNEL = "channel-type." + BINDING_ID + ".";
public class OptionEntry {
public ChannelTypeUID uid;
public String key;
public String value;
public OptionEntry(ChannelTypeUID uid, String key, String value) {
this.uid = uid;
this.key = key;
this.value = value;
}
}
private final CopyOnWriteArrayList<OptionEntry> stateOptions = new CopyOnWriteArrayList<>();
private static final ChannelMap CHANNEL_DEFINITIONS = new ChannelMap();
@Activate
@@ -169,8 +187,6 @@ public class ShellyChannelDefinitions {
.add(new ShellyChannel(m, CHGR_LIGHTCH, CHANNEL_TIMER_AUTOOFF, "timerAutoOff", ITEMT_TIME))
.add(new ShellyChannel(m, CHGR_LIGHTCH, CHANNEL_TIMER_ACTIVE, "timerActive", ITEMT_SWITCH))
// RGBW2-color
.add(new ShellyChannel(m, CHGR_LIGHT, CHANNEL_LIGHT_POWER, "system:power", ITEMT_SWITCH))
// Power Meter
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_CURRENTWATTS, "meterWatts", ITEMT_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_METER_TOTALKWH, "meterTotal", ITEMT_ENERGY))
@@ -182,7 +198,7 @@ public class ShellyChannelDefinitions {
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_REACTWATTS, "meterReactive", ITEMT_POWER))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_VOLTAGE, "meterVoltage", ITEMT_VOLT))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_CURRENT, "meterCurrent", ITEMT_AMP))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_PFACTOR, "meterPowerFactor", ITEMT_PERCENT))
.add(new ShellyChannel(m, CHGR_METER, CHANNEL_EMETER_PFACTOR, "meterPowerFactor", ITEMT_NUMBER))
// Sensors
.add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TEMP, "sensorTemp", ITEMT_TEMP))
@@ -227,11 +243,11 @@ public class ShellyChannelDefinitions {
// TRV
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_POSITION, "sensorPosition", ITEMT_DIMMER))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_MODE, "controlMode", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_SCHEDULE, "controlSchedule", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_PROFILE, "controlProfile", ITEMT_STRING))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_SETTEMP, "targetTemp", ITEMT_TEMP))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_BCONTROL, "boostControl", ITEMT_SWITCH))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_BTIMER, "boostTimer", ITEMT_TIME));
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_BTIMER, "boostTimer", ITEMT_TIME))
.add(new ShellyChannel(m, CHGR_CONTROL, CHANNEL_CONTROL_SCHEDULE, "controlSchedule", ITEMT_SWITCH));
}
public static @Nullable ShellyChannel getDefinition(String channelName) throws IllegalArgumentException {
@@ -273,7 +289,8 @@ public class ShellyChannelDefinitions {
addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
if (!profile.isSensor && !profile.isIX && getDouble(status.temperature) != SHELLY_API_INVTEMP) {
if (!profile.isSensor && !profile.isIX && status.temperature != null
&& status.temperature != SHELLY_API_INVTEMP) {
// Only some devices report the internal device temp
addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP);
}
@@ -311,19 +328,20 @@ public class ShellyChannelDefinitions {
if (profile.settings.relays != null) {
ShellySettingsRelay rs = profile.settings.relays.get(idx);
boolean timer = rs.hasTimer != null || rstatus.hasTimer != null; // Dimmer 1/2 have
addChannel(thing, add, rs.ison != null, group, CHANNEL_OUTPUT);
addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
boolean timer = rs.hasTimer != null || rstatus.hasTimer != null; // Dimmer 1/2 have
addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
addChannel(thing, add, rs.autoOn != null, group, CHANNEL_TIMER_AUTOON);
addChannel(thing, add, rs.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
}
// Shelly 1/1PM Addon
if (profile.settings.extTemperature != null) {
addChannel(thing, add, profile.settings.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1);
addChannel(thing, add, profile.settings.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2);
addChannel(thing, add, profile.settings.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3);
if (profile.status.extTemperature != null) {
addChannel(thing, add, profile.status.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP1);
addChannel(thing, add, profile.status.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP2);
addChannel(thing, add, profile.status.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENDOR_TEMP3);
}
if (profile.settings.extHumidity != null) {
addChannel(thing, add, profile.settings.extHumidity.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENDOR_HUMIDITY);
@@ -344,7 +362,6 @@ public class ShellyChannelDefinitions {
ShellySettingsDimmer ds = profile.settings.dimmers.get(idx);
addChannel(thing, add, ds.autoOn != null, group, CHANNEL_TIMER_AUTOON);
addChannel(thing, add, ds.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
ShellyShortLightStatus dss = dstatus.dimmers.get(idx);
addChannel(thing, add, dss != null && dss.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
}
@@ -359,7 +376,6 @@ public class ShellyChannelDefinitions {
if (profile.settings.lights != null) {
ShellySettingsRgbwLight light = profile.settings.lights.get(idx);
String whiteGroup = profile.isRGBW2 ? group : CHANNEL_GROUP_WHITE_CONTROL;
// Create power channel in color mode and brightness channel in white mode
addChannel(thing, add, profile.inColor, group, CHANNEL_LIGHT_POWER);
addChannel(thing, add, light.autoOn != null, group, CHANNEL_TIMER_AUTOON);
@@ -368,29 +384,31 @@ public class ShellyChannelDefinitions {
addChannel(thing, add, light.brightness != null, whiteGroup, CHANNEL_BRIGHTNESS);
addChannel(thing, add, light.temp != null, whiteGroup, CHANNEL_COLOR_TEMP);
}
return add;
}
public static Map<String, Channel> createInputChannels(final Thing thing, final ShellyDeviceProfile profile,
final ShellySettingsStatus status, String group) {
final ShellySettingsStatus status) {
Map<String, Channel> add = new LinkedHashMap<>();
if (status.inputs != null) {
// Create channels per input. For devices with more than 1 input (Dimmer, 1L) multiple channel sets are
// created by adding the index to the channel name
boolean multi = (profile.numRelays == 1 || profile.isDimmer || profile.isRoller) && profile.numInputs >= 2;
for (int i = 0; i < profile.numInputs; i++) {
String suffix = multi ? String.valueOf(i + 1) : "";
ShellyInputState input = status.inputs.get(i);
String group = profile.getInputGroup(i);
String suffix = profile.getInputSuffix(i); // multi ? String.valueOf(i + 1) : "";
addChannel(thing, add, true, group, CHANNEL_INPUT + suffix);
addChannel(thing, add, true, group,
(!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
if (profile.inButtonMode(i)) {
ShellyInputState input = status.inputs.get(i);
addChannel(thing, add, input.event != null, group, CHANNEL_STATUS_EVENTTYPE + suffix);
addChannel(thing, add, input.eventCount != null, group, CHANNEL_STATUS_EVENTCOUNT + suffix);
}
addChannel(thing, add, true, group,
(!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
}
} else if (status.input != null) {
// old RGBW2 firmware
String group = profile.getInputGroup(0);
addChannel(thing, add, true, group, CHANNEL_INPUT);
addChannel(thing, add, true, group, CHANNEL_BUTTON_TRIGGER);
}
@@ -409,7 +427,7 @@ public class ShellyChannelDefinitions {
ShellyThingInterface handler = (ShellyThingInterface) thing.getHandler();
if (handler != null) {
ShellySettingsGlobal settings = handler.getProfile().settings;
if (getBool(settings.favoritesEnabled) && (settings.favorites != null)) {
if (getBool(settings.favoritesEnabled) && settings.favorites != null) {
addChannel(thing, add, roller.currentPos != null, CHGR_ROLLER, CHANNEL_ROL_CONTROL_FAV);
}
}
@@ -435,7 +453,7 @@ public class ShellyChannelDefinitions {
addChannel(thing, newChannels, emeter.reactive != null, group, CHANNEL_EMETER_REACTWATTS);
addChannel(thing, newChannels, emeter.voltage != null, group, CHANNEL_EMETER_VOLTAGE);
addChannel(thing, newChannels, emeter.current != null, group, CHANNEL_EMETER_CURRENT);
addChannel(thing, newChannels, emeter.power != null, group, CHANNEL_EMETER_PFACTOR); // EM has no PF. but power
addChannel(thing, newChannels, emeter.pf != null, group, CHANNEL_EMETER_PFACTOR); // EM has no PF. but power
addChannel(thing, newChannels, true, group, CHANNEL_LAST_UPDATE);
return newChannels;
@@ -498,7 +516,7 @@ public class ShellyChannelDefinitions {
addChannel(thing, newChannels, true, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_MODE);
addChannel(thing, newChannels, true, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE);
addChannel(thing, newChannels, true, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SCHEDULE);
addChannel(thing, newChannels, true, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE); // TRV
addChannel(thing, newChannels, true, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE);
}
// Battery
@@ -550,6 +568,30 @@ public class ShellyChannelDefinitions {
}
}
public List<StateOption> getStateOptions(ChannelTypeUID uid) {
List<StateOption> options = new ArrayList<>();
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
options.add(new StateOption(oe.key, oe.value));
}
}
return options;
}
public void addStateOption(String channelId, String key, String value) {
ChannelTypeUID uid = getChannelTypeUID(channelId);
stateOptions.addIfAbsent(new OptionEntry(uid, key, value));
}
public void clearStateOptions(String channelId) {
ChannelTypeUID uid = getChannelTypeUID(channelId);
for (OptionEntry oe : stateOptions) {
if (oe.uid.equals(uid)) {
stateOptions.remove(oe);
}
}
}
public class ShellyChannel {
private final ShellyTranslationProvider messages;
public String group = "";

View File

@@ -13,17 +13,29 @@
package org.openhab.binding.shelly.internal.provider;
import static org.openhab.binding.shelly.internal.ShellyBindingConstants.BINDING_ID;
import java.util.List;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateOption;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* This class provides the list of valid inputs for the input channel of a source.
@@ -32,27 +44,39 @@ import org.openhab.core.types.StateDescription;
*
*/
@NonNullByDefault
public class ShellyStateDescriptionProvider extends BaseDynamicStateDescriptionProvider implements ThingHandlerService {
private @Nullable ShellyThingInterface handler;
@Component(service = { DynamicStateDescriptionProvider.class, ShellyStateDescriptionProvider.class })
public class ShellyStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
private final ThingRegistry thingRegistry;
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = (ShellyThingInterface) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return (ThingHandler) handler;
@Activate
public ShellyStateDescriptionProvider(final @Reference EventPublisher eventPublisher, //
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, //
final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService,
@Reference ThingRegistry thingRegistry) {
this.eventPublisher = eventPublisher;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
this.thingRegistry = thingRegistry;
}
@SuppressWarnings("null")
@Override
public @Nullable StateDescription getStateDescription(Channel channel, @Nullable StateDescription original,
@Nullable Locale locale) {
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
ChannelTypeUID uid = channel.getChannelTypeUID();
if (uid != null && handler != null) {
setStateOptions(channel.getUID(), handler.getStateOptions(uid));
if (uid == null || !BINDING_ID.equals(uid.getBindingId()) || originalStateDescription == null) {
return null;
}
return super.getStateDescription(channel, original, locale);
Thing thing = thingRegistry.get(channel.getUID().getThingUID());
ShellyThingInterface handler = (ShellyThingInterface) thing.getHandler();
if (handler == null) {
return null;
}
List<StateOption> stateOptions = handler.getStateOptions(uid);
return stateOptions == null ? null
: StateDescriptionFragmentBuilder.create(originalStateDescription).withOptions(stateOptions).build()
.toStateDescription();
}
}

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:shelly:relay-gen2">
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
<description>@text/thing-type.config.shelly.password.description</description>
<context>password</context>
</parameter>
<parameter name="updateInterval" type="integer" min="10" required="true" unit="s">
<label>@text/thing-type.config.shelly.updateInterval.label</label>
<description>@text/thing-type.config.shelly.updateInterval.description</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:roller-gen2">
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
<description>@text/thing-type.config.shelly.password.description</description>
<context>password</context>
</parameter>
<parameter name="favoriteUP" type="integer" min="0" max="4" required="false">
<label>@text/thing-type.config.shelly.roller.favoriteUP.label</label>
<description>@text/thing-type.config.shelly.roller.favoriteUP.description</description>
<default>0</default>
</parameter>
<parameter name="favoriteDOWN" type="integer" min="0" max="4" required="false">
<label>@text/thing-type.config.shelly.roller.favoriteDOWN.label</label>
<description>@text/thing-type.config.shelly.roller.favoriteDOWN.description</description>
<default>0</default>
</parameter>
<parameter name="updateInterval" type="integer" min="10" required="true" unit="s">
<label>@text/thing-type.config.shelly.updateInterval.label</label>
<description>@text/thing-type.config.shelly.updateInterval.description</description>
<default>60</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:shelly:battery-gen2">
<parameter name="deviceIp" type="text" required="true">
<label>@text/thing-type.config.shelly.deviceIp.label</label>
<description>@text/thing-type.config.shelly.deviceIp.description</description>
<context>network-address</context>
</parameter>
<parameter name="password" type="text" required="false">
<label>@text/thing-type.config.shelly.password.label</label>
<description>@text/thing-type.config.shelly.password.description</description>
<context>password</context>
</parameter>
<parameter name="lowBattery" type="integer" required="false">
<label>@text/thing-type.config.shelly.battery.lowBattery.label</label>
<description>@text/thing-type.config.shelly.battery.lowBattery.description</description>
<default>20</default>
<unitLabel>%</unitLabel>
</parameter>
<parameter name="updateInterval" type="integer" min="60" required="true" unit="s">
<label>@text/thing-type.config.shelly.updateInterval.label</label>
<description>@text/thing-type.config.shelly.updateInterval.description</description>
<default>900</default>
<unitLabel>seconds</unitLabel>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -17,9 +17,10 @@ message.config-status.error.missing-userid = No user ID in the Thing configurati
# Thing status descriptions
message.offline.conf-error-no-credentials = Device is password protected, but no credentials have been configured.
message.offline.conf-error-access-denied = Access denied, check user id and password.
message.offline.conf-error-wrong-mode = Device is no longer in the configured device mode {0}, required {1}. Delete the thing and re-discover the device.
message.offline.conf-error-wrong-mode = Device is in device mode {0}, thing requires {1}. Delete the thing and re-discover the device.
message.offline.status-error-connect = Unable to connect to device - {0}
message.offline.status-error-timeout = Device is not reachable (API timeout)
message.offline.status-error-unexpected-error = Unexpected error
message.offline.status-error-unexpected-error = Unexpected error: {0}
message.offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
message.offline.status-error-watchdog = Device is not responding, seems to be unavailable.
message.offline.status-error-restarted = The device has restarted and will be re-initialized.
@@ -27,8 +28,8 @@ message.offline.status-error-fwupgrade = Firmware upgrade in progress
# General messages
message.versioncheck.failed = Unable to check firmware version: {0}
message.versioncheck.beta = Device is running a Beta version: {0}/{1}.
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, required minimal {3}.
message.versioncheck.beta = Device is running a Beta version: {0}/{1}
message.versioncheck.tooold = WARNING: Firmware might be too old, installed: {0}/{1}, minimal required {2}
message.versioncheck.update = INFO: New firmware available: current version: {0}, new version: {1}
message.versioncheck.autocoiot = INFO: Firmware is full-filling the minimum version to auto-enable CoIoT
message.init.noipaddress = Unable to detect local IP address. Please make sure that IPv4 is enabled for this interface and check openHAB Network Configuration.
@@ -76,7 +77,7 @@ thing-type.shelly.shellysmoke.description = Shelly Smoke Sensor (battery powered
thing-type.shelly.shellyflood.description = Shelly Flood Sensor (battery powered)
thing-type.shelly.shellysense.description = Shelly Sense (Motion Sensor and Remote IR Controller)
thing-type.shelly.shellybulb.description = Shelly Bulb (Dimmable RGB+W Bulb)
thing-type.shelly.shellybulbduo.description = Shelly Duo (Dimmable WW/CW Bulb)
thing-type.shelly.shellybulbduo.description = Shelly Duo (Dimmable RGB+W/CW Bulb)
thing-type.shelly.shellycolorbulb.description = Shelly Color Bulb (Dimmable RGB+W Bulb)
thing-type.shelly.shellyvintage.description = Shelly Vintage (Dimmable Vintage Bulb)
thing-type.shelly.shellyrgbw2-color.description = Shelly RGBW2 (LED Controller in Color Mode)
@@ -85,6 +86,24 @@ thing-type.shelly.shellymotion.description = Shelly Motion (Motion Detection (ba
thing-type.shelly.shellygas.description = Shelly Gas (Gas Sensor - Gas Leak Detection)
thing-type.shelly.shellytrv.description = Shelly TRV (Radiator value, battery powered)
thing-type.shelly.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
thing-type.shelly.shellypludht.description = Shelly Plus HT - Temperature and Humidity Sensor
# Plus/Pro devices
thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
thing-type.shelly.shellyplus1pm.description = Shelly Plus 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch
thing-type.shelly.shellypro2pm-relay.description= Shelly Pro 2PM - Dual Relay Switch with Power Meter
thing-type.shelly.shellypro2pm-roller.description = Shelly Pro 2PM - Roller Control with Power Meter
thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
# Plus/Pro devices
thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
@@ -180,11 +199,11 @@ channel-group-type.shelly.relayChannel3.label = Relay 3
channel-group-type.shelly.relayChannel4.label = Relay 4
channel-group-type.shelly.dimmerChannel.label = Dimmer
channel-group-type.shelly.dimmerChannel.description = A Shelly Dimmer channel
channel-group-type.shelly.ix3Channel1.label = Input 1
channel-group-type.shelly.ix3Channel2.label = Input 2
channel-group-type.shelly.ix3Channel3.label = Input 3
channel-group-type.shelly.ix3Channel4.label = Input 4
channel-group-type.shelly.ix3Channel.description = Input Status
channel-group-type.shelly.iXChannel1.label = Input 1
channel-group-type.shelly.iXChannel2.label = Input 2
channel-group-type.shelly.iXChannel3.label = Input 3
channel-group-type.shelly.iXChannel4.label = Input 3
channel-group-type.shelly.iXChannel.description = Input Status
channel-group-type.shelly.rollerControl.label = Roller Control
channel-group-type.shelly.rollerControl.description = Controlling the roller mode
channel-group-type.shelly.meter.label = Power Meter
@@ -200,9 +219,9 @@ channel-group-type.shelly.externalSensors.description = Temperatures from extern
channel-type.shelly.outputName.label = Output Name
channel-type.shelly.outputName.description = Output/Channel Name as configured in the Shelly App
channel-type.shelly.timerAutoOn.label = Auto-ON Timer
channel-type.shelly.timerAutoOn.description = When the output of the relay is switched off, it will be switched off automatically after n seconds
channel-type.shelly.timerAutoOn.description = When the output of the relay is switched off, it will be switched back on automatically after n seconds
channel-type.shelly.timerAutoOff.label = Auto-OFF Timer
channel-type.shelly.timerAutoOff.description = When the output of the relay is switched on, it will be switched on automatically after n seconds
channel-type.shelly.timerAutoOff.description = When the output of the relay is switched on, it will be switched back off automatically after n seconds
channel-type.shelly.timerActive.label = Auto ON/OFF timer active
channel-type.shelly.timerActive.description = ON: A timer is active, OFF: no timer active
channel-type.shelly.temperature.label = Temperature
@@ -398,7 +417,7 @@ channel-type.shelly.boostTimer.label = Boost Timer
channel-type.shelly.boostTimer.description = Number of minutes to boost maximum power
channel-type.shelly.batVoltage.label = Battery Voltage
channel-type.shelly.batVoltage.description = Battery voltage (V)
channel-type.shelly.charger.label = Charger Connected
channel-type.shelly.charger.label = External Power
channel-type.shelly.charger.description = ON: External power is connected
channel-type.shelly.calibrated.label = Calibrated
channel-type.shelly.calibrated.description = ON: The device/sensor is calibrated
@@ -429,7 +448,7 @@ channel-type.shelly.lastEvent.state.option.L = Long push
channel-type.shelly.lastEvent.state.option.SL = Short-Long push
channel-type.shelly.lastEvent.state.option.LS = Long-Short push
channel-type.shelly.eventCount.label = Event Count
channel-type.shelly.eventCount.description = Event Count
channel-type.shelly.eventCount.description = Number of input events received from device
channel-type.shelly.eventTrigger.label = Event
channel-type.shelly.eventTrigger.description = Event Trigger (ROLLER_OPEN/ROLLER_CLOSE/ROLLER_STOP)
channel-type.shelly.eventTrigger.option.ROLLER_OPEN = Roller is open
@@ -506,6 +525,18 @@ message.manager.action.setwifirec-enable = The device performs an auto-restart i
message.manager.action.setwifirec-disable = WiFi Recovery Mode will be disabled.
message.manager.action.setwifirec-confirm = WiFi Recovery Mode has been {0}.
message.manager.action.setwifirec-failed = Unable to update setting for WiFi Recovery Mode: {0}
message.manager.action.setwifirangeext-enable = WiFi Range Extender will be enabled.
message.manager.action.setwifirangeext-disable = WiFi Range Extender will be disabled.
message.manager.action.setwifirangeext-confirm = WiFiRange Extender has been set, restart required: {0}
message.manager.action.setwifirangeext-failed = Unable to update setting for WiFi Range Extender: {0}
message.manager.action.setethernet-enable = Ethernet interface will be enabled, this requires a device reboot.
message.manager.action.setethernet-disable = Ethernet interface will be disabled, this may require a device reboot.
message.manager.action.setethernet-confirm = Ethernet interface has been {0}.
message.manager.action.setethernet-failed = Unable to update setting for Ethernet interface: {0}
message.manager.action.setbluetooth-enable = Bluetooth interface will be enabled, this requires a device reboot.
message.manager.action.setbluetooth-disable = Bluetooth interface will be disabled, this may require a device reboot.
message.manager.action.setbluetooth-confirm = Bluetooth interface has been {0}.
message.manager.action.setbluetooth-failed = Unable to update setting for Bluetooth interface: {0}
message.manager.action.aproaming-enable = WiFi Access Point Roaming will be enabled. Check product documentation for details.
message.manager.action.aproaming-disable = WiFi Access Point Roaming will be disabled.
message.manager.action.aproaming-confirm = Unable to update setting WiFi Access Point Roaming: {0}

View File

@@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellybulb">
<label>Shelly Bulb (SHBLB-1)</label>
<label>Shelly Bulb</label>
<description>@text/thing-type.shelly.shellybulb.description</description>
<category>Lightbulb</category>
<channel-groups>
@@ -20,7 +20,7 @@
</thing-type>
<thing-type id="shellybulbduo">
<label>Shelly Duo (SHBDUO-1)</label>
<label>Shelly Duo</label>
<description>@text/thing-type.shelly.shellybulbduo.description</description>
<category>Lightbulb</category>
<channel-groups>
@@ -35,7 +35,7 @@
</thing-type>
<thing-type id="shellycolorbulb">
<label>Shelly Color Bulb (SHCB-1)</label>
<label>Shelly Color Bulb</label>
<description>@text/thing-type.shelly.shellycolorbulb.description</description>
<category>ColorLight</category>
<channel-groups>
@@ -51,7 +51,7 @@
</thing-type>
<thing-type id="shellyvintage">
<label>Shelly Vintage (SHVIN-1)</label>
<label>Shelly Vintage</label>
<description>@text/thing-type.shelly.shellyvintage.description</description>
<category>Lightbulb</category>
<channel-groups>
@@ -66,7 +66,7 @@
</thing-type>
<thing-type id="shellyrgbw2-color">
<label>Shelly RGBW2 Color Mode (SHRGBW2)</label>
<label>Shelly RGBW2 Color Mode</label>
<description>@text/thing-type.shelly.shellyrgbw2-color.description</description>
<category>ColorLight</category>
<channel-groups>
@@ -81,7 +81,7 @@
</thing-type>
<thing-type id="shellyrgbw2-white">
<label>Shelly RGBW2 White Mode (SHRGBW2)</label>
<label>Shelly RGBW2 White Mode</label>
<description>@text/thing-type.shelly.shellyrgbw2-white.description</description>
<category>Lightbulb</category>
<channel-groups>

View File

@@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shelly1">
<label>Shelly 1 (SHSW-1)</label>
<label>Shelly 1</label>
<description>@text/thing-type.shelly.shelly1.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
@@ -18,7 +18,7 @@
</thing-type>
<thing-type id="shelly1l">
<label>Shelly 1L (SHSW-L)</label>
<label>Shelly 1L</label>
<description>@text/thing-type.shelly.shelly1l.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
@@ -32,7 +32,7 @@
</thing-type>
<thing-type id="shelly1pm">
<label>Shelly 1PM (SHSW-PM)</label>
<label>Shelly 1PM</label>
<description>@text/thing-type.shelly.shelly1pm.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
@@ -46,7 +46,7 @@
</thing-type>
<thing-type id="shellyem">
<label>Shelly EM (SHEM)</label>
<label>Shelly EM</label>
<description>@text/thing-type.shelly.shellyem.description</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
@@ -65,7 +65,7 @@
<!-- Shelly 3EM - device reports wrong service name: shellyem3, see README -->
<thing-type id="shellyem3">
<label>Shelly 3EM (SHEM-3)</label>
<label>Shelly EM3</label>
<description>@text/thing-type.shelly.shellyem3.description</description>
<channel-groups>
<channel-group id="meter1" typeId="meter">
@@ -86,7 +86,7 @@
</thing-type>
<thing-type id="shelly2-relay">
<label>Shelly 2 Relay (SHSW-21)</label>
<label>Shelly 2 Relay</label>
<description>@text/thing-type.shelly.shelly2-relay.description</description>
<channel-groups>
@@ -104,22 +104,8 @@
<config-description-ref uri="thing-type:shelly:relay"/>
</thing-type>
<thing-type id="shelly2-roller">
<label>Shelly 2 Roller (SHSW-21)</label>
<description>@text/thing-type.shelly.shelly2-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:roller"/>
</thing-type>
<thing-type id="shelly25-relay">
<label>Shelly 2.5 Relay (SHSW-25)</label>
<label>Shelly 2.5 Relay</label>
<description>@text/thing-type.shelly.shelly25-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
@@ -142,7 +128,7 @@
</thing-type>
<thing-type id="shelly25-roller">
<label>Shelly 2.5 Roller (SHSW-25)</label>
<label>Shelly 2.5 Roller</label>
<description>@text/thing-type.shelly.shelly25-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
@@ -156,7 +142,7 @@
</thing-type>
<thing-type id="shelly4pro">
<label>Shelly 4 Pro Relay (SHSW-44)</label>
<label>Shelly 4 Pro Relay</label>
<description>@text/thing-type.shelly.shelly4pro.description</description>
<channel-groups>
@@ -192,7 +178,7 @@
</thing-type>
<thing-type id="shellyplug">
<label>Shelly Plug (SHPLG-1)</label>
<label>Shelly Plug</label>
<description>@text/thing-type.shelly.shellyplug.description</description>
<category>PowerOutlet</category>
<channel-groups>
@@ -206,7 +192,7 @@
</thing-type>
<thing-type id="shellyplugs">
<label>Shelly Plug-S (SHPLG-S)</label>
<label>Shelly Plug-S</label>
<description>@text/thing-type.shelly.shellyplugs.description</description>
<category>PowerOutlet</category>
<channel-groups>
@@ -220,7 +206,7 @@
</thing-type>
<thing-type id="shellyplugu1">
<label>Shelly Plug US (SHPLG-U1)</label>
<label>Shelly Plug US</label>
<description>@text/thing-type.shelly.shellyplugu1.description</description>
<category>PowerOutlet</category>
<channel-groups>
@@ -234,7 +220,7 @@
</thing-type>
<thing-type id="shellyuni">
<label>Shelly UNI (SHUNI-1)</label>
<label>Shelly UNI</label>
<description>@text/thing-type.shelly.shellyuni.description</description>
<channel-groups>
@@ -253,7 +239,7 @@
</thing-type>
<thing-type id="shellydimmer">
<label>Shelly Dimmer (SHDM-1)</label>
<label>Shelly Dimmer</label>
<description>@text/thing-type.shelly.shellydimmer.description</description>
<category>DimmableLight</category>
<channel-groups>
@@ -267,7 +253,7 @@
</thing-type>
<thing-type id="shellydimmer2">
<label>Shelly Dimmer 2 (SHDM-2)</label>
<label>Shelly Dimmer 2</label>
<description>@text/thing-type.shelly.shellydimmer2.description</description>
<category>DimmableLight</category>
<channel-groups>
@@ -281,17 +267,17 @@
</thing-type>
<thing-type id="shellyix3">
<label>Shelly ix3 (SHIX3-1)</label>
<label>Shelly ix3</label>
<description>@text/thing-type.shelly.shellyix3.description</description>
<channel-groups>
<channel-group id="status1" typeId="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel1.label</label>
<channel-group id="status1" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel1.label</label>
</channel-group>
<channel-group id="status2" typeId="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel2.label</label>
<channel-group id="status2" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel2.label</label>
</channel-group>
<channel-group id="status3" typeId="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel3.label</label>
<channel-group id="status3" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel3.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
@@ -315,9 +301,9 @@
<description>@text/channel-group-type.shelly.dimmerChannel.description</description>
</channel-group-type>
<channel-group-type id="ix3Channel">
<label>@text/channel-group-type.shelly.ix3Channel.label</label>
<description>@text/channel-group-type.shelly.ix3Channel.description</description>
<channel-group-type id="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel.label</label>
<description>@text/channel-group-type.shelly.ixChannel.description</description>
</channel-group-type>
<channel-group-type id="rollerControl">
@@ -576,7 +562,7 @@
<item-type>Number</item-type>
<label>@text/channel-type.shelly.meterPowerFactor.label</label>
<description>@text/channel-type.shelly.meterPowerFactor.description</description>
<state readOnly="true" pattern="%.3f %unit%">
<state readOnly="true" pattern="%.3f">
</state>
</channel-type>

View File

@@ -5,7 +5,7 @@
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyht">
<label>Shelly H&amp;T (SHHT-1)</label>
<label>Shelly H&amp;T</label>
<description>@text/thing-type.shelly.shellyht.description</description>
<category>Sensor</category>
<channel-groups>
@@ -33,7 +33,7 @@
</thing-type>
<thing-type id="shellygas">
<label>Shelly GAS (SHGS-1)</label>
<label>Shelly Gas</label>
<description>@text/thing-type.shelly.shellygas.description</description>
<category>Sensor</category>
<channel-groups>
@@ -46,7 +46,7 @@
</thing-type>
<thing-type id="shellyflood">
<label>Shelly Flood (SHWT-1)</label>
<label>Shelly Flood</label>
<description>@text/thing-type.shelly.shellyflood.description</description>
<category>Sensor</category>
<channel-groups>
@@ -60,7 +60,7 @@
</thing-type>
<thing-type id="shellydw">
<label>Shelly Door/Window (SHDW-1)</label>
<label>Shelly Door/Window</label>
<description>@text/thing-type.shelly.shellydw.description</description>
<category>Sensor</category>
<channel-groups>
@@ -74,7 +74,7 @@
</thing-type>
<thing-type id="shellydw2">
<label>Shelly Door/Window (SHDW-2)</label>
<label>Shelly Door/Window</label>
<description>@text/thing-type.shelly.shellydw2.description</description>
<category>Sensor</category>
<channel-groups>
@@ -88,7 +88,7 @@
</thing-type>
<thing-type id="shellysense">
<label>Shelly Sense (SHSEN-1)</label>
<label>Shelly Sense</label>
<description>@text/thing-type.shelly.shellysense.description</description>
<channel-groups>
<channel-group id="control" typeId="control"/>
@@ -102,7 +102,7 @@
</thing-type>
<thing-type id="shellybutton1">
<label>Shelly Button 1 (SHBTN-1)</label>
<label>Shelly Button 1</label>
<description>@text/thing-type.shelly.shellybutton1.description</description>
<category>WallSwitch</category>
<channel-groups>
@@ -116,7 +116,7 @@
</thing-type>
<thing-type id="shellybutton2">
<label>Shelly Button 2 (SHBTN-2)</label>
<label>Shelly Button 2</label>
<description>@text/thing-type.shelly.shellybutton2.description</description>
<category>WallSwitch</category>
<channel-groups>
@@ -130,7 +130,7 @@
</thing-type>
<thing-type id="shellymotion">
<label>Shelly Motion (SHMOS-01/SHMOS-02)</label>
<label>Shelly Motion</label>
<description>@text/thing-type.shelly.shellymotion.description</description>
<category>MotionDetector</category>
<channel-groups>
@@ -144,7 +144,7 @@
</thing-type>
<thing-type id="shellytrv">
<label>Shelly TRV (SHTRV-01)</label>
<label>Shelly TRV</label>
<description>@text/thing-type.shelly.shellytrv.description</description>
<category>RadiatorControl</category>
<channel-groups>
@@ -210,7 +210,6 @@
<state readOnly="false"></state>
</channel-type>
<channel-group-type id="sensorData">
<label>@text/channel-group-type.shelly.sensorData.label</label>
<description>@text/channel-group-type.shelly.sensorData.description</description>
@@ -457,7 +456,7 @@
<item-type>Number:ElectricPotential</item-type>
<label>@text/channel-type.shelly.sensorADC.label</label>
<description>@text/channel-type.shelly.sensorADC.description</description>
<state readOnly="true" pattern="%.0f %unit%">
<state readOnly="true" pattern="%.3f %unit%">
</state>
</channel-type>

View File

@@ -0,0 +1,259 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyplus1">
<label>ShellyPlus 1</label>
<description>@text/thing-type.shelly.shellyplus1.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplus1pm">
<label>ShellyPlus 1PM</label>
<description>@text/thing-type.shelly.shellyplus1pm.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplus2pm-relay">
<label>ShellyPlus 2 Relay</label>
<description>@text/thing-type.shelly.shellyplus2-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel"/>
<channel-group id="meter1" typeId="meter"/>
<channel-group id="relay2" typeId="relayChannel"/>
<channel-group id="meter2" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplus2pm-roller">
<label>ShellyPlus 2PM Roller</label>
<description>@text/thing-type.shelly.shellyplus2pm-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:roller-gen2"/>
</thing-type>
<thing-type id="shellyplusi4">
<label>ShellyPlus i4</label>
<description>@text/thing-type.shelly.shellyplusi4.description</description>
<channel-groups>
<channel-group id="status1" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel1.label</label>
</channel-group>
<channel-group id="status2" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel2.label</label>
</channel-group>
<channel-group id="status3" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel3.label</label>
</channel-group>
<channel-group id="status4" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellyplusi4dc">
<label>ShellyPlus i4DC</label>
<description>@text/thing-type.shelly.shellyplusi4dc.description</description>
<channel-groups>
<channel-group id="status1" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel1.label</label>
</channel-group>
<channel-group id="status2" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel2.label</label>
</channel-group>
<channel-group id="status3" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel3.label</label>
</channel-group>
<channel-group id="status4" typeId="ixChannel">
<label>@text/channel-group-type.shelly.ixChannel4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro1">
<label>ShellyPro 1</label>
<description>@text/thing-type.shelly.shellypro1.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro1pm">
<label>ShellyPro 1PM</label>
<description>@text/thing-type.shelly.shellypro1pm.description</description>
<channel-groups>
<channel-group id="relay" typeId="relayChannel"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="sensors" typeId="externalSensors"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro2-relay">
<label>ShellyPro 2 Relay</label>
<description>@text/thing-type.shelly.shellypro2-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<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-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro2pm-relay">
<label>ShellyPro 2PM Relay</label>
<description>@text/thing-type.shelly.shellypro2pm-relay.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<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.meter2.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro2pm-roller">
<label>ShellyPro 2PM Roller</label>
<description>@text/thing-type.shelly.shellypro2pm-roller.description</description>
<category>Rollershutter</category>
<channel-groups>
<channel-group id="roller" typeId="rollerControl"/>
<channel-group id="meter" typeId="meter"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:roller-gen2"/>
</thing-type>
<thing-type id="shellypro3">
<label>ShellyPro 3</label>
<description>@text/thing-type.shelly.shellypro3.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<channel-group id="relay2" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel2.label</label>
</channel-group>
<channel-group id="relay3" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel3.label</label>
</channel-group>
<channel-group id="relay4" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
<thing-type id="shellypro4pm">
<label>ShellyPro 4PM</label>
<description>@text/thing-type.shelly.shellypro4pm.description</description>
<channel-groups>
<channel-group id="relay1" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel1.label</label>
</channel-group>
<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.meter2.label</label>
</channel-group>
<channel-group id="relay3" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel3.label</label>
</channel-group>
<channel-group id="meter3" typeId="meter">
<label>@text/channel-group-type.shelly.meter3.label</label>
</channel-group>
<channel-group id="relay4" typeId="relayChannel">
<label>@text/channel-group-type.shelly.relayChannel4.label</label>
</channel-group>
<channel-group id="meter4" typeId="meter">
<label>@text/channel-group-type.shelly.meter4.label</label>
</channel-group>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:relay-gen2"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="shelly"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="shellyplusht">
<label>ShellyPlus H&amp;T</label>
<description>@text/thing-type.shelly.shellypludht.description</description>
<category>Sensor</category>
<channel-groups>
<channel-group id="sensors" typeId="sensorData"/>
<channel-group id="battery" typeId="batteryStatus"/>
<channel-group id="device" typeId="deviceStatus"/>
</channel-groups>
<representation-property>serviceName</representation-property>
<config-description-ref uri="thing-type:shelly:battery-gen2"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -56,9 +56,9 @@
</td>
<td>
<div title="Refresh Device Status">
<a href="${uri}/overview?action=refresh&uid=${uid}">
<img src="${uri}/images/refresh.png" class="icon"/>
</a>
<a href="${uri}/overview?action=refresh&uid=${uid}">
<img src="${uri}/images/refresh.png" class="icon"/>
</a>
</div>
</td>
<td>
@@ -101,6 +101,6 @@
<td align="right">${deviceRestarts}</td>
<td align="right">${timeoutErrors}</td>
<td align="right">${timeoutsRecovered}</td>
<td align="right" title="CoIoT Status: ${coiotStatus}">${coiotMessages}</td>
<td align="right">${coiotErrors}</td>
<td align="right" title="CoIOT Status: ${coiotStatus}">${protocolMessages}</td>
<td align="right">${protocolErrors}</td>
</tr>

View File

@@ -55,6 +55,6 @@
<th>Device Restarts</th>
<th>Timeout Errors</th>
<th>Timeouts Recovered</th>
<th>CoIOT Messages</th>
<th>CoIOT Errors</th>
<th>Protocol Messages</th>
<th>Protocol Errors</th>
</tr>