diff --git a/bundles/org.openhab.binding.shelly/README.md b/bundles/org.openhab.binding.shelly/README.md index 5750f1ff200fb..dee49a1585f33 100644 --- a/bundles/org.openhab.binding.shelly/README.md +++ b/bundles/org.openhab.binding.shelly/README.md @@ -1,6 +1,7 @@ # Shelly Binding This Binding integrates [Shelly devices](https://shelly.cloud) devloped by Allterco. + ![](https://shop.shelly.cloud/image/cache/catalog/shelly_1/s1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_dimmer2/shelly_dimmer2_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_vintage/shelly_vintage_A60-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_plug_s/s_plug_s_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_button1/shelly_button1_x1-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_gas/shelly_gas_eu-80x80.jpg) ![](https://shop.shelly.cloud/image/cache/catalog/shelly_ht/s_ht_x1-80x80.jpg) Allterco provides a rich set of smart home devices. All of them are WiFi enabled (2,4GHz, IPv4 only) and provide a documented API. @@ -793,6 +794,10 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa ### Shelly Motion (thing-type: shellymotion) +Important: The Shelly Motion does only support CoIoT Unicast, which means you need to set the CoIoT peer address. + +Use device WebUI, open COIOT settings, make sure CoIoT is enabled and enter the openHAB IP address or + |Group |Channel |Type |read-only|Description | |----------|---------------|---------|---------|---------------------------------------------------------------------| |sensors |motion |Switch |yes |ON: Motion was detected | @@ -801,10 +806,17 @@ You can define 2 items (1 Switch, 1 Number) mapping to the same channel, see exa | |illumination |String |yes |Current illumination: dark/twilight/bright | | |vibration |Switch |yes |ON: Vibration detected | | |charger |Switch |yes |ON: USB charging cable is connected external power supply activated. | +| |motionActive |Switch |yes |ON: Motion detection is currently active | +| |sensorSleepTime|Number |no |Specifies the number of sec the sensor should not report events ] | |lastUpdate |DateTime |yes |Timestamp of the last update (any sensor value changed) | |battery |batteryLevel |Number |yes |Battery Level in % | | |lowBattery |Switch |yes |Low battery alert (< 20%) | +Use case for the 'sensorSleepTime': +You have a Motion controlling your light. +You switch off the light and want to leave the room, but the motion sensor immediately switches light back on. +Using 'sensorSleepTime' you could suppress motion events while leaving the room, e.g. for 5sec and the light doesn's switch on. + ### Shelly Button 1 (thing-type: shellybutton1) |Group |Channel |Type |read-only|Description | diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png b/bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png new file mode 100644 index 0000000000000..0b50b67050fbd Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview.png new file mode 100644 index 0000000000000..5cf5f4b0f682f Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png new file mode 100644 index 0000000000000..70fdfd6e200fd Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png new file mode 100644 index 0000000000000..0be8e1b6b55a0 Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png new file mode 100644 index 0000000000000..f265710f0a1e0 Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png differ diff --git a/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png new file mode 100644 index 0000000000000..c01a79fbe9999 Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png differ diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java index e17a1c7157659..adb8ab68d76e9 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java @@ -249,8 +249,10 @@ public class ShellyBindingConstants { public static final String CHANNEL_SENSOR_VALVE = "valve"; public static final String CHANNEL_SENSOR_SSTATE = "status"; // Shelly Gas public static final String CHANNEL_SENSOR_ALARM_STATE = "alarmState"; + public static final String CHANNEL_SENSOR_MOTION_ACT = "motionActive"; public static final String CHANNEL_SENSOR_MOTION = "motion"; public static final String CHANNEL_SENSOR_MOTION_TS = "motionTimestamp"; + public static final String CHANNEL_SENSOR_SLEEPTIME = "sensorSleepTime"; public static final String CHANNEL_SENSOR_ERROR = "lastError"; // External sensors for Shelly1/1PM diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java index 5708d30ca6f19..737094e72c7b6 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java @@ -14,6 +14,7 @@ import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*; +import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -25,6 +26,7 @@ import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler; import org.openhab.binding.shelly.internal.handler.ShellyLightHandler; +import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface; import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler; import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler; import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider; @@ -141,8 +143,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return null; } - public Map getThingHandlers() { - return deviceListeners; + public Map getThingHandlers() { + return new HashMap<>(deviceListeners); } /** diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java index bb460bb32944f..d4be3688a9d26 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.List; +import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings; import org.openhab.core.thing.CommonTriggerEvents; import com.google.gson.annotations.SerializedName; @@ -226,6 +227,12 @@ public class ShellyApiJsonDTO { public static final String SHELLY_TEMP_CELSIUS = "C"; public static final String SHELLY_TEMP_FAHRENHEIT = "F"; + // Motion + public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset + + // CoIoT Multicast setting + public static final String SHELLY_COIOT_MCAST = "mcast"; + public static class ShellySettingsDevice { public String type; public String mac; @@ -267,7 +274,7 @@ public static class ShellySettingsWiFiNetwork { } public static class ShellySettingsMqtt { - public Boolean enabled; + public Boolean enable; public String server; public String user; @SerializedName("reconnect_timeout_max") @@ -292,10 +299,17 @@ public static class ShellySettingsMqtt { public static class ShellySettingsCoiot { // FW 1.6+ @SerializedName("update_period") public Integer updatePeriod; + public Boolean enabled; // Motion 1.0.7: Coap can be disabled + public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast + } + + public static class ShellyStatusMqtt { + public Boolean connected; } public static class ShellySettingsSntp { public String server; + public Boolean enabled; } public static class ShellySettingsLogin { @@ -319,10 +333,6 @@ public static class ShellyStatusCloud { public Boolean connected; } - public static class ShellyStatusMqtt { - public Boolean connected; - } - public static class ShellySettingsHwInfo { @SerializedName("hw_revision") public String hwRevision; @@ -532,8 +542,11 @@ public static class ShellySettingsGlobal { public ShellySettingsWiFiNetwork wifiSta; @SerializedName("wifi_sta1") public ShellySettingsWiFiNetwork wifiSta1; - // public ShellySettingsMqtt mqtt; // not used for now - // public ShellySettingsSntp sntp; // not used for now + @SerializedName("wifirecovery_reboot_enabled") + public Boolean wifiRecoveryReboot; + + public ShellySettingsMqtt mqtt; // not used for now + public ShellySettingsSntp sntp; // not used for now public ShellySettingsCoiot coiot; // Firmware 1.6+ public ShellySettingsLogin login; @SerializedName("pin_code") @@ -544,8 +557,8 @@ public static class ShellySettingsGlobal { public Boolean discoverable; // FW 1.6+ public String fw; @SerializedName("build_info") - ShellySettingsBuildInfo buildInfo; - ShellyStatusCloud cloud; + public ShellySettingsBuildInfo buildInfo; + public ShellyStatusCloud cloud; @SerializedName("sleep_mode") public ShellySensorSleepMode sleepMode; // FW 1.6 @SerializedName("external_power") @@ -626,6 +639,18 @@ public static class ShellySettingsGlobal { @SerializedName("favorites_enabled") public Boolean favoritesEnabled; public ArrayList favorites; + + // Motion + public ShellyMotionSettings motion; + @SerializedName("tamper_sensitivity") + public Integer tamperSensitivity; + @SerializedName("dark_threshold") + public Integer darkThreshold; + @SerializedName("twilight_threshold") + public Integer twilightThreshold; + + @SerializedName("sleep_time") // Shelly Motion + public Integer sleepTime; } public static class ShellySettingsAttributes { @@ -644,11 +669,17 @@ public static class ShellySettingsAttributes { public String fw; // current FW version } + public static class ShellyActionsStats { + public Integer skipped; + } + public static class ShellySettingsStatus { public String name; // FW 1.8: Symbolic Device name is configurable @SerializedName("wifi_sta") public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details + public ShellyStatusCloud cloud; + public ShellyStatusMqtt mqtt; public String time; public Integer serial; @@ -658,6 +689,8 @@ public static class ShellySettingsStatus { public Boolean discoverable; // FW 1.6+ @SerializedName("cfg_changed_cnt") public Integer cfgChangedCount; // FW 1.8 + @SerializedName("actions_stats") + public ShellyActionsStats astats; public ArrayList relays; public ArrayList rollers; @@ -690,6 +723,9 @@ public static class ShellySettingsStatus { public Long fsFree; public Long uptime; + @SerializedName("sleep_time") // Shelly Motion + public Integer sleepTime; + public String json; } @@ -908,6 +944,17 @@ public static class ShellySensorAccel { public Integer vibration; // Whether vibration is detected } + public static class ShellyMotionSettings { + public Integer sensitivity; + @SerializedName("blind_time_minutes") + public Integer blindTimeMinutes; + @SerializedName("pulse_count") + public Integer pulseCount; + @SerializedName("operating_mode") + public Integer operatingMode; + public Boolean enabled; + } + public static class ShellyExtTemperature { public static class ShellyShortTemp { public Double tC; // temperature in deg C @@ -953,8 +1000,6 @@ public static class ShellyADC { public Boolean motion; // Shelly Sense: true=motion detected public Boolean charger; // Shelly Sense: true=charger connected - @SerializedName("external_power") - public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense @SerializedName("act_reasons") public List actReasons; // HT/Smoke/Flood: list of reasons which woke up the device diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java index 74895c0e86ee5..2d4a832e6e316 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java @@ -18,8 +18,11 @@ import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal; import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput; @@ -41,6 +44,7 @@ @NonNullByDefault public class ShellyDeviceProfile { private final Logger logger = LoggerFactory.getLogger(ShellyDeviceProfile.class); + private final static Pattern VERSION_PATTERN = Pattern.compile("v\\d+\\.\\d+\\.\\d+"); public boolean initialized = false; // true when initialized @@ -54,6 +58,8 @@ public class ShellyDeviceProfile { public String hostname = ""; public String mode = ""; public boolean discoverable = true; + public boolean auth = false; + public boolean alwaysOn = true; public String hwRev = ""; public String hwBatchId = ""; @@ -115,11 +121,11 @@ public ShellyDeviceProfile initialize(String thingType, String json) throws Shel hostname = settings.device.hostname != null && !settings.device.hostname.isEmpty() ? settings.device.hostname.toLowerCase() : "shelly-" + mac.toUpperCase().substring(6, 11); - mode = !getString(settings.mode).isEmpty() ? getString(settings.mode).toLowerCase() : ""; + mode = getString(settings.mode).toLowerCase(); hwRev = settings.hwinfo != null ? getString(settings.hwinfo.hwRevision) : ""; hwBatchId = settings.hwinfo != null ? getString(settings.hwinfo.batchId.toString()) : ""; fwDate = substringBefore(settings.fw, "/"); - fwVersion = substringBetween(settings.fw, "/", "@"); + fwVersion = extractFwVersion(settings.fw); fwId = substringAfter(settings.fw, "@"); discoverable = (settings.discoverable == null) || settings.discoverable; @@ -129,8 +135,6 @@ public ShellyDeviceProfile initialize(String thingType, String json) throws Shel if ((numRelays > 0) && (settings.relays == null)) { numRelays = 0; } - isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2); - isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER); hasRelays = (numRelays > 0) || isDimmer; numRollers = getInteger(settings.device.numRollers); numInputs = settings.inputs != null ? settings.inputs.size() : hasRelays ? isRoller ? 2 : 1 : 0; @@ -177,6 +181,9 @@ public void initFromThingType(String name) { return; } + isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2); + isRoller = mode.equalsIgnoreCase(SHELLY_MODE_ROLLER); + isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR); isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR) || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR); @@ -198,14 +205,14 @@ public void initFromThingType(String name) { isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR); isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR); isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense; - hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion; // we assume that Sense is connected to - // the charger + hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion; + + alwaysOn = !hasBattery || isMotion || isSense; // true means: device is reachable all the time (no sleep mode) } public void updateFromStatus(ShellySettingsStatus status) { if (hasRelays) { - // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after - // initialization + // Dimmer-2 doesn't report inputs under /settings, only on /status, we need to update that info after init if (status.inputs != null) { numInputs = status.inputs.size(); } @@ -317,4 +324,24 @@ public int getRollerFav(int id) { } return -1; } + + public static String extractFwVersion(@Nullable String version) { + if (version != null) { + Matcher matcher = VERSION_PATTERN.matcher(version); + if (matcher.find()) { + // e.g. 20210226-091047/v1.10.0-rc2-89-g623b41ec0-master + return matcher.group(0); + } + } + return ""; + } + + public boolean coiotEnabled() { + if ((settings.coiot != null) && (settings.coiot.enabled != null)) { + return settings.coiot.enabled; + } + + // If device is not yet intialized or the enabled property is missing we assume that CoIoT is enabled + return true; + } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java index 19ef1f1e05d82..c57d9ef00a048 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java @@ -194,26 +194,28 @@ public ShellyStatusSensor getSensorStatus() throws ShellyApiException { .convert(getDouble(status.tmp.value)); status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f; } - if ((status.charger == null) && (status.externalPower != null)) { + if ((status.charger == null) && (profile.settings.externalPower != null)) { // SHelly H&T uses external_power, Sense uses charger - status.charger = status.externalPower != 0; + status.charger = profile.settings.externalPower != 0; } - return status; } - public void setTimer(Integer index, String timerName, Double value) throws ShellyApiException { + public void setTimer(int index, String timerName, int 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 + "=" - + ((Integer) value.intValue()).toString(); + String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value; request(uri); } + public void setSleepTime(int value) throws ShellyApiException { + request(SHELLY_URL_SETTINGS + "?sleep_time=" + value); + } + public void setLedStatus(String ledName, Boolean value) throws ShellyApiException { request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE)); } @@ -239,6 +241,10 @@ public ShellySettingsLogin setLoginCredentials(String user, String password) thr ShellySettingsLogin.class); } + public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException { + return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class); + } + public String deviceReboot() throws ShellyApiException { return callApi(SHELLY_URL_RESTART, String.class); } @@ -251,6 +257,10 @@ public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException return callApi("/ota?" + uri, ShellySettingsUpdate.class); } + public String setCloud(boolean enabled) throws ShellyApiException { + return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class); + } + /** * Change between White and Color Mode * diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java index 8fed7e315a080..a239f41e0f2fa 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java @@ -111,8 +111,12 @@ public boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen break; case "1103": // roller_0: S, rollerPos, 0-100, unknown -1 int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS)); + logger.debug("{}: CoAP update roller position: control={}, position={}", thingName, + SHELLY_MAX_ROLLER_POS - pos, pos); updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL, toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT)); + updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS, + toQuantityType((double) pos, Units.PERCENT)); break; case "1105": // S, valvle, closed/opened/not_connected/failure/closing/opening/checking or unbknown updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr)); @@ -304,6 +308,8 @@ public boolean handleStatusUpdate(List sensorUpdates, CoIotDescrSen break; case "3120": // motionActive // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1}, + updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT, + getTimestamp(getString(profile.settings.timezone), (long) s.value)); break; case "6108": // A, gas, none/mild/heavy/test or unknown diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java index db02cc289059a..064621e57b51c 100755 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java @@ -38,6 +38,7 @@ import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; import org.openhab.binding.shelly.internal.api.ShellyHttpApi; import org.openhab.binding.shelly.internal.coap.ShellyCoapHandler; +import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO; import org.openhab.binding.shelly.internal.coap.ShellyCoapServer; import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration; import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration; @@ -72,7 +73,7 @@ * @author Markus Michels - Initial contribution */ @NonNullByDefault -public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceListener { +public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceListener, ShellyManagerInterface { protected final Logger logger = LoggerFactory.getLogger(ShellyBaseHandler.class); protected final ShellyChannelDefinitions channelDefinitions; @@ -216,7 +217,7 @@ private boolean initializeThing() throws ShellyApiException { // 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.hasBattery && !profile.isMotion && !profile.isSense) { + if (config.eventsCoIoT && !profile.alwaysOn) { coap.start(thingName, config); } @@ -242,6 +243,7 @@ private boolean initializeThing() throws ShellyApiException { // New Shelly devices might use a different endpoint for the CoAP listener tmpPrf.coiotEndpoint = devInfo.coiot; } + tmpPrf.auth = devInfo.auth; // missing in /settings logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})", thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion, @@ -249,18 +251,34 @@ private boolean initializeThing() throws ShellyApiException { logger.debug("{}: Shelly settings info for {}: {}", thingName, tmpPrf.hostname, tmpPrf.settingsJson); logger.debug("{}: Device " + "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{})" - + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}" - + ",updatePeriod:{}sec", thingName, tmpPrf.hasRelays, tmpPrf.numRelays, tmpPrf.isRoller, + + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}" + + ",alwaysOn:{}, ,updatePeriod:{}sec", thingName, tmpPrf.hasRelays, tmpPrf.numRelays, tmpPrf.isRoller, tmpPrf.numRollers, tmpPrf.isDimmer, tmpPrf.numMeters, tmpPrf.isEMeter, tmpPrf.isSensor, tmpPrf.isDW, tmpPrf.hasBattery, tmpPrf.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "", - tmpPrf.isSense, tmpPrf.isLight, profile.isBulb, tmpPrf.isDuo, tmpPrf.isRGBW2, tmpPrf.inColor, - tmpPrf.updatePeriod); + tmpPrf.isSense, tmpPrf.isMotion, tmpPrf.isLight, profile.isBulb, tmpPrf.isDuo, tmpPrf.isRGBW2, + tmpPrf.inColor, tmpPrf.alwaysOn, tmpPrf.updatePeriod); // update thing properties tmpPrf.status = api.getStatus(); tmpPrf.updateFromStatus(tmpPrf.status); 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 + ":" + ShellyCoapJSonDTO.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.debug("{}: Unable to set CoIoT peer: {}", thingName, e.toString()); + } + } else if (!devpeer.equals(ourpeer)) { + logger.warn("{}: CoIoT peer in device settings does not point this to this host, disabling CoIoT", + thingName); + config.eventsCoIoT = autoCoIoT = false; + } + } if (autoCoIoT) { logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName); config.eventsCoIoT = true; @@ -327,6 +345,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { api.setLedStatus(SHELLY_LED_POWER_DISABLE, command == OnOffType.ON); break; + case CHANNEL_SENSOR_SLEEPTIME: + logger.debug("{}: Set sensor sleep time to {}", thingName, command); + int value = ((DecimalType) command).intValue(); + value = value > 0 ? Math.max(SHELLY_MOTION_SLEEPTIME_OFFSET, value - SHELLY_MOTION_SLEEPTIME_OFFSET) + : 0; + api.setSleepTime(value); + break; + default: update = handleDeviceCommand(channelUID, command); break; @@ -368,10 +394,9 @@ protected void refreshStatus() { initializeThing(); // may fire an exception if initialization failed } // Get profile, if refreshSettings == true reload settings from device - profile = getProfile(refreshSettings); - - logger.trace("{}: Updating status", thingName); + logger.trace("{}: Updating status (refreshSettings={})", thingName, refreshSettings); ShellySettingsStatus status = api.getStatus(); + profile = getProfile(refreshSettings || checkRestarted(status)); profile.status = status; profile.updateFromStatus(status); @@ -446,16 +471,18 @@ public boolean isThingOffline() { return getThing().getStatus() == ThingStatus.OFFLINE; } + @Override public void setThingOnline() { if (!isThingOnline()) { updateStatus(ThingStatus.ONLINE); // request 3 updates in a row (during the first 2+3*3 sec) - requestUpdates(!profile.hasBattery ? 3 : 1, channelsCreated == false); + requestUpdates(profile.alwaysOn ? 3 : 1, channelsCreated == false); } restartWatchdog(); } + @Override public void setThingOffline(ThingStatusDetail detail, String messageKey) { if (!isThingOffline()) { logger.info("{}: Thing goes OFFLINE: {}", thingName, messages.get(messageKey)); @@ -485,8 +512,9 @@ private boolean isWatchdogStarted() { } public void reinitializeThing() { + logger.debug("{}: Re-Initialize Thing", thingName); updateStatus(ThingStatus.UNKNOWN); - requestUpdates(1, true); + requestUpdates(0, true); } private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { @@ -495,6 +523,7 @@ private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { // Update uptime and WiFi, internal temp ShellyComponents.updateDeviceStatus(this, status); + stats.wifiRssi = status.wifiSta.rssi; if (api.isInitialized()) { stats.timeoutErrors = api.getTimeoutErrors(); @@ -503,15 +532,9 @@ private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { stats.remainingWatchdog = watchdog > 0 ? now() - watchdog : 0; // Check various device indicators like overheating - logger.debug("{}: status.update={}, lastUpdate={}", thingName, status.uptime, stats.lastUptime); - if ((status.uptime < stats.lastUptime) && profile.isInitialized()) { - alarm = ALARM_TYPE_RESTARTED; - force = true; - stats.unexpectedRestarts++; - logger.debug("{}: Device restart #{} detected", thingName, stats.unexpectedRestarts); - + if (checkRestarted(status)) { // Force re-initialization on next status update - if (!profile.hasBattery || profile.isMotion) { + if (profile.alwaysOn) { reinitializeThing(); } } else if (getBool(status.overtemperature)) { @@ -521,6 +544,15 @@ private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { } else if (getBool(status.loaderror)) { alarm = ALARM_TYPE_LOADERR; } + State internalTemp = getChannelValue(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP); + if (internalTemp != UnDefType.NULL) { + int temp = ((Number) internalTemp).intValue(); + if (temp > stats.maxInternalTemp) { + logger.debug("{}: Max Internal Temp for device changed to {}", thingName, temp); + stats.maxInternalTemp = temp; + } + } + stats.lastUptime = getLong(status.uptime); stats.coiotMessages = coap.getMessageCount(); stats.coiotErrors = coap.getErrorCount(); @@ -530,6 +562,24 @@ private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) { } } + /** + * Check if device has restarted and needs a new Thing initialization + * + * @return true: restart detected + */ + + private boolean checkRestarted(ShellySettingsStatus status) { + if (profile.isInitialized() && (status.uptime < stats.lastUptime || !profile.status.update.oldVersion.isEmpty() + && !status.update.oldVersion.equals(profile.status.update.oldVersion))) { + logger.debug("{}: Device restart #{} detected", thingName, stats.restarts); + stats.restarts++; + postEvent(ALARM_TYPE_RESTARTED, true); + updateProperties(profile, status); + return true; + } + return false; + } + /** * Save alarm to the lastAlarm channel * @@ -752,7 +802,7 @@ private void checkVersion(ShellyDeviceProfile prf, ShellySettingsStatus status) logger.info("{}: {}", prf.hostname, messages.get("versioncheck.beta", prf.fwVersion, prf.fwDate, prf.fwId, SHELLY_API_MIN_FWVERSION)); } else { - if (version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) { + if ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWVERSION) < 0) && !profile.isMotion) { logger.warn("{}: {}", prf.hostname, messages.get("versioncheck.tooold", prf.fwVersion, prf.fwDate, prf.fwId, SHELLY_API_MIN_FWVERSION)); } @@ -838,13 +888,14 @@ protected void startUpdateJob() { * @return true=Update schedule, false=skipped (too many updates already * scheduled) */ + @Override public boolean requestUpdates(int requestCount, boolean refreshSettings) { this.refreshSettings |= refreshSettings; if (refreshSettings) { if (requestCount == 0) { logger.debug("{}: Request settings refresh", thingName); } - scheduledUpdates = requestCount; + scheduledUpdates = 1; return true; } if (scheduledUpdates < 10) { // < 30s @@ -920,7 +971,7 @@ public void triggerButton(String group, int idx, String value) { profile.isRoller ? CHANNEL_EVENT_TRIGGER : CHANNEL_BUTTON_TRIGGER + profile.getInputSuffix(idx), trigger); updateChannel(group, CHANNEL_LAST_UPDATE, getTimestamp()); - if (!profile.hasBattery) { + if (profile.alwaysOn) { // refresh status of the input channel requestUpdates(1, false); } @@ -941,6 +992,7 @@ public boolean updateChannel(String channelId, State value, boolean force) { return !stopping && cache.updateChannel(channelId, value, force); } + @Override public State getChannelValue(String group, String channel) { return cache.getValue(group, channel); } @@ -1005,6 +1057,7 @@ public boolean areChannelsCreated() { * @param status the /status result */ protected void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) { + logger.debug("{}: Update properties", thingName); Map properties = fillDeviceProperties(profile); String serviceName = getString(getThing().getProperties().get(PROPERTY_SERVICE_NAME)); String hostname = getString(profile.settings.device.hostname).toLowerCase(); @@ -1112,6 +1165,7 @@ public static Map fillDeviceProperties(ShellyDeviceProfile profi * @return ShellyDeviceProfile instance * @throws ShellyApiException */ + @Override public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException { try { refreshSettings |= forceRefresh; @@ -1127,6 +1181,7 @@ public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiExce return profile; } + @Override public ShellyDeviceProfile getProfile() { return profile; } @@ -1185,16 +1240,28 @@ public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiE return false; } + @Override + public String getThingName() { + return thingName; + } + + @Override + public void resetStats() { + // reset statistics + stats = new ShellyDeviceStats(); + } + + @Override public ShellyDeviceStats getStats() { return stats; } - public Map getStatsProp() { - return stats.asProperties(getString(profile.settings.timezone)); + @Override + public ShellyHttpApi getApi() { + return api; } - public void resetStats() { - // reset statistics - stats = new ShellyDeviceStats(); + public Map getStatsProp() { + return stats.asProperties(); } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java index 7fff96aae96c2..fa0cc13335712 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java @@ -64,6 +64,9 @@ public static boolean updateDeviceStatus(ShellyBaseHandler thingHandler, ShellyS thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP, toQuantityType(getDouble(status.temperature), DIGITS_NONE, SIUnits.CELSIUS)); } + thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SLEEPTIME, + toQuantityType(getInteger(status.sleepTime), Units.SECOND)); + thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPDATE, getOnOff(status.hasUpdate)); return false; // device status never triggers update @@ -212,7 +215,7 @@ public static boolean updateMeters(ShellyBaseHandler thingHandler, ShellySetting updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH, toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR)); - if (updated) { + if (updated && timestamp > 0) { thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp(getString(profile.settings.timezone), timestamp)); } @@ -247,7 +250,6 @@ public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettin boolean updated = false; if (profile.isSensor || profile.hasBattery) { ShellyStatusSensor sdata = thingHandler.api.getSensorStatus(); - if (!thingHandler.areChannelsCreated()) { thingHandler.logger.trace("{}: Create missing sensor channel(s)", thingHandler.thingName); thingHandler.updateChannelDefinitions( @@ -355,6 +357,8 @@ public static boolean updateSensors(ShellyBaseHandler thingHandler, ShellySettin getOnOff(sdata.motion)); } if (sdata.sensor != null) { // Shelly Motion + updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT, + getOnOff(sdata.sensor.motionActive)); updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION, getOnOff(sdata.sensor.motion)); long timestamp = getLong(sdata.sensor.motionTimestamp); diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java index 7611aa9ffff20..002b37e458a3d 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java @@ -26,7 +26,7 @@ @NonNullByDefault public class ShellyDeviceStats { public long lastUptime = 0; - public long unexpectedRestarts = 0; + public long restarts = 0; public long timeoutErrors = 0; public long timeoutsRecorvered = 0; public long remainingWatchdog = 0; @@ -35,20 +35,22 @@ public class ShellyDeviceStats { public long lastAlarmTs = 0; public long coiotMessages = 0; public long coiotErrors = 0; + public int wifiRssi = 0; + public int maxInternalTemp = 0; - public Map asProperties(String timeZone) { + public Map asProperties() { Map prop = new HashMap<>(); prop.put("lastUptime", String.valueOf(lastUptime)); - prop.put("unexpectedRestarts", String.valueOf(unexpectedRestarts)); + prop.put("deviceRestarts", String.valueOf(restarts)); prop.put("timeoutErrors", String.valueOf(timeoutErrors)); prop.put("timeoutsRecovered", String.valueOf(timeoutsRecorvered)); prop.put("remainingWatchdog", String.valueOf(remainingWatchdog)); prop.put("alarmCount", String.valueOf(alarms)); prop.put("lastAlarm", lastAlarm); - prop.put("lastAlarmTs", - lastAlarmTs != 0 ? ShellyUtils.getTimestamp(timeZone, lastAlarmTs).format(null).replace('T', ' ') : ""); + prop.put("lastAlarmTs", ShellyUtils.convertTimestamp(lastAlarmTs)); prop.put("coiotMessages", String.valueOf(coiotMessages)); prop.put("coiotErrors", String.valueOf(coiotErrors)); + prop.put("wifiRssi", String.valueOf(wifiRssi)); return prop; } } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyManagerInterface.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyManagerInterface.java new file mode 100644 index 0000000000000..d1bb1a5952367 --- /dev/null +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyManagerInterface.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2021 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.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.shelly.internal.api.ShellyApiException; +import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile; +import org.openhab.binding.shelly.internal.api.ShellyHttpApi; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.State; + +/** + * The {@link ShellyManagerInterface} implements the interface for Shelly Manager to access the thing handler + * + * @author Markus Michels - Initial contribution + */ +@NonNullByDefault +public interface ShellyManagerInterface { + + public Thing getThing(); + + public String getThingName(); + + public ShellyDeviceProfile getProfile(); + + public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException; + + public ShellyHttpApi getApi(); + + public ShellyDeviceStats getStats(); + + public void resetStats(); + + public State getChannelValue(String group, String channel); + + public void setThingOnline(); + + public void setThingOffline(ThingStatusDetail detail, String messageKey); + + public boolean requestUpdates(int requestCount, boolean refreshSettings); +} diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyRelayHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyRelayHandler.java index e60878e9a55cc..b11d6a840b6b4 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyRelayHandler.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyRelayHandler.java @@ -134,11 +134,11 @@ public boolean handleDeviceCommand(ChannelUID channelUID, Command command) throw case CHANNEL_TIMER_AUTOON: logger.debug("{}: Set Auto-ON timer to {}", thingName, command); - api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command)); + api.setTimer(rIndex, SHELLY_TIMER_AUTOON, getNumber(command).intValue()); break; case CHANNEL_TIMER_AUTOOFF: logger.debug("{}: Set Auto-OFF timer to {}", thingName, command); - api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command)); + api.setTimer(rIndex, SHELLY_TIMER_AUTOOFF, getNumber(command).intValue()); break; } return true; @@ -219,7 +219,7 @@ public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiE */ private void handleRoller(Command command, String groupName, Integer index, boolean isControl) throws ShellyApiException { - Integer position = -1; + int position = -1; if ((command instanceof UpDownType) || (command instanceof OnOffType)) { ShellyControlRoller rstatus = api.getRollerStatus(index); @@ -235,29 +235,33 @@ && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) { } } - if (command == UpDownType.UP || command == OnOffType.ON) { + if (command == UpDownType.UP || command == OnOffType.ON + || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 100))) { logger.debug("{}: Open roller", thingName); - api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN); - int pos = profile.getRollerFav(config.favoriteUP - 1); - position = pos > 0 ? pos : SHELLY_MAX_ROLLER_POS; - if (pos > 0) { + int shpos = profile.getRollerFav(config.favoriteUP - 1); + if (shpos > 0) { logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP, - pos); + shpos); + api.setRollerPos(index, shpos); + position = shpos; + } else { + api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN); + position = SHELLY_MIN_ROLLER_POS; } - } else if (command == UpDownType.DOWN || command == OnOffType.OFF) { + } else if (command == UpDownType.DOWN || command == OnOffType.OFF + || ((command instanceof DecimalType) && (((DecimalType) command).intValue() == 0))) { logger.debug("{}: Closing roller", thingName); - int pos = profile.getRollerFav(config.favoriteDOWN - 1); - if (pos > 0) { + int shpos = profile.getRollerFav(config.favoriteDOWN - 1); + if (shpos > 0) { // use favorite position - if (pos > 0) { - logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName, - config.favoriteDOWN, pos); - } - api.setRollerPos(index, pos); + logger.debug("{}: Use favoriteDOWN id {} for positioning roller ({}%)", thingName, + config.favoriteDOWN, shpos); + api.setRollerPos(index, shpos); + position = shpos; } else { api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_CLOSE); + position = SHELLY_MAX_ROLLER_POS; } - position = SHELLY_MAX_ROLLER_POS - pos; } } else if (command == StopMoveType.STOP) { logger.debug("{}: Stop roller", thingName); @@ -275,8 +279,8 @@ && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) { "Invalid value type for roller control/position" + command.getClass().toString()); } - // take position from RollerShutter control and map to Shelly positon (OH: - // 0=closed, 100=open; Shelly 0=open, 100=closed) + // take position from RollerShutter control and map to Shelly positon + // OH: 0=closed, 100=open; Shelly 0=open, 100=closed) // take position 1:1 from position channel position = isControl ? SHELLY_MAX_ROLLER_POS - position : position; validateRange("roller position", position, SHELLY_MIN_ROLLER_POS, SHELLY_MAX_ROLLER_POS); @@ -284,12 +288,15 @@ && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_CLOSE))) { 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)); } } @@ -399,6 +406,8 @@ public boolean updateRelays(ShellySettingsStatus status) throws ShellyApiExcepti String state = getString(control.state); if (state.equals(SHELLY_ALWD_ROLLER_TURN_STOP)) { // only valid in stop state int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(control.currentPos, SHELLY_MAX_ROLLER_POS)); + logger.debug("{}: REST Update roller position: control={}, position={}", thingName, + SHELLY_MAX_ROLLER_POS - pos, pos); updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL, toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT)); updated |= updateChannel(groupName, CHANNEL_ROL_CONTROL_POS, diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyChannelDefinitions.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyChannelDefinitions.java index a4c4672e8daad..e3e4308a3ae06 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyChannelDefinitions.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyChannelDefinitions.java @@ -100,7 +100,7 @@ public class ShellyChannelDefinitions { public ShellyChannelDefinitions(@Reference ShellyTranslationProvider translationProvider) { ShellyTranslationProvider m = translationProvider; - // Device: Internal Temp + // Device CHANNEL_DEFINITIONS // Device .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEMT_STRING)) @@ -179,12 +179,14 @@ public ShellyChannelDefinitions(@Reference ShellyTranslationProvider translation .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_TILT, "sensorTilt", ITEMT_ANGLE)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION, "sensorMotion", ITEMT_SWITCH)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION_TS, "motionTimestamp", ITEMT_DATETIME)) + .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_MOTION_ACT, "motionActive", ITEMT_SWITCH)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_FLOOD, "sensorFlood", ITEMT_SWITCH)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SMOKE, "sensorSmoke", ITEMT_SWITCH)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_PPM, "sensorPPM", ITEMT_NUMBER)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_VALVE, "sensorValve", ITEMT_STRING)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ALARM_STATE, "alarmState", ITEMT_STRING)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_ERROR, "sensorError", ITEMT_STRING)) + .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_SENSOR_SLEEPTIME, "sensorSleepTime", ITEMT_NUMBER)) .add(new ShellyChannel(m, CHGR_SENSOR, CHANNEL_LAST_UPDATE, "lastUpdate", ITEMT_DATETIME)) // Button/ix3 @@ -249,6 +251,7 @@ public static Map createDeviceChannels(final Thing thing, final addChannel(thing, add, (status.tmp != null) || (status.temperature != null), CHGR_DEVST, CHANNEL_DEVST_ITEMP); } + addChannel(thing, add, profile.settings.sleepTime != null, CHGR_SENSOR, CHANNEL_SENSOR_SLEEPTIME); // If device has more than 1 meter the channel accumulatedWatts receives the accumulated value boolean accuChannel = (((status.meters != null) && (status.meters.size() > 1) && !profile.isRoller @@ -259,13 +262,9 @@ public static Map createDeviceChannels(final Thing thing, final addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPDATE); addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_UPTIME); addChannel(thing, add, true, CHGR_DEVST, CHANNEL_DEVST_HEARTBEAT); - - if (profile.settings.ledPowerDisable != null) { - addChannel(thing, add, true, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE); - } - if (profile.settings.ledStatusDisable != null) { - addChannel(thing, add, true, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE); // WiFi status LED - } + addChannel(thing, add, profile.settings.ledPowerDisable != null, CHGR_DEVST, CHANNEL_LED_POWER_DISABLE); + addChannel(thing, add, profile.settings.ledPowerDisable != null, CHGR_DEVST, CHANNEL_LED_STATUS_DISABLE); // WiFi + // return add; } @@ -417,6 +416,8 @@ public static Map createSensorChannels(final Thing thing, final CHANNEL_SENSOR_MOTION); if (sdata.sensor != null) { // DW, Sense or Motion addChannel(thing, newChannels, sdata.sensor.state != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT); // DW/DW2 + addChannel(thing, newChannels, sdata.sensor.motionActive != null, CHANNEL_GROUP_SENSOR, // Motion + CHANNEL_SENSOR_MOTION_ACT); addChannel(thing, newChannels, sdata.sensor.motionTimestamp != null, CHANNEL_GROUP_SENSOR, // Motion CHANNEL_SENSOR_MOTION_TS); addChannel(thing, newChannels, sdata.sensor.vibration != null, CHANNEL_GROUP_SENSOR, diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyTranslationProvider.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyTranslationProvider.java index b39225c99a3aa..ebc56a0a16889 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyTranslationProvider.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyTranslationProvider.java @@ -16,6 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.shelly.internal.util.ShellyUtils; import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; import org.osgi.framework.Bundle; @@ -44,14 +45,15 @@ public ShellyTranslationProvider(@Reference TranslationProvider i18nProvider, this.localeProvider = localeProvider; } - public @Nullable String get(String key, @Nullable Object... arguments) { + public String get(String key, @Nullable Object... arguments) { return getText(key.contains("@text/") || key.contains(".shelly.") ? key : "message." + key, arguments); } - public @Nullable String getText(String key, @Nullable Object... arguments) { + public String getText(String key, @Nullable Object... arguments) { try { Locale locale = localeProvider.getLocale(); - return i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments); + String message = i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments); + return ShellyUtils.getString(message); } catch (IllegalArgumentException e) { return "Unable to load message for key " + key; } diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyUtils.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyUtils.java index 36b6a605e7f5a..6f10ac16c0a03 100644 --- a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyUtils.java +++ b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyUtils.java @@ -24,6 +24,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import javax.measure.Unit; @@ -53,6 +54,7 @@ @NonNullByDefault public class ShellyUtils { private final static String PRE = "Unable to create object of type "; + public final static DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern(DateTimeType.DATE_PATTERN); public static T fromJson(Gson gson, @Nullable String json, Class classOfT) throws ShellyApiException { @Nullable @@ -256,7 +258,7 @@ public static DateTimeType getTimestamp() { public static DateTimeType getTimestamp(String zone, long timestamp) { try { if (timestamp == 0) { - return getTimestamp(); + throw new IllegalArgumentException("Timestamp value 0 is invalid"); } ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault(); ZonedDateTime zdt = LocalDateTime.now().atZone(zoneId); @@ -268,6 +270,18 @@ public static DateTimeType getTimestamp(String zone, long timestamp) { } } + public static String getTimestamp(DateTimeType dt) { + return dt.getZonedDateTime().toString().replace('T', ' ').replace('-', '/'); + } + + public static String convertTimestamp(long ts) { + if (ts == 0) { + return ""; + } + String time = DATE_TIME.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(ts), ZoneId.systemDefault())); + return time.replace('T', ' ').replace('-', '/'); + } + public static Integer getLightIdFromGroup(String groupName) { if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) { return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1; diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties index 3e2dd791a3262..e4b70f7aaa1cf 100644 --- a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties +++ b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties @@ -34,7 +34,7 @@ message.status.unknown.initializing = Initializing or device in sleep mode. message.statusupdate.failed = Unable to update status message.event.triggered = Event triggered: {0} message.coap.init.failed = Unable to start CoIoT: {0} -message.discovery.disabled = Device is marked as non-discoverable -> skip +message.discovery.disabled = Device is marked as non-discoverable, will be skipped message.discovery.protected = Device {0} reported 'Access defined' (missing userid/password or incorrect). message.discovery.failed = Device discovery of device with IP address {0} failed: {1} message.roller.favmissing = Roller position favorites are not supported by installed firmware or not configured in the Shelly App @@ -42,6 +42,8 @@ message.roller.favmissing = Roller position favorites are not supported by insta # Device channel-type.shelly.deviceName.label = Device Name channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App. +channel-type.shelly.sensorSleepTime.label = Sensor Sleep Time +channel-type.shelly.sensorSleepTime.description = The sensor will not send notifications and will not perform actions until the specified time expires. (0=disable) # Relay, external sensors channel-type.shelly.outputName.label = Output Name @@ -54,6 +56,8 @@ channel-type.shelly.temperature3.label = Temperature 3 channel-type.shelly.temperature3.description = Temperature of external Sensor #3 channel-type.shelly.humidity.label = Humidity channel-type.shelly.humidity.description = Relative humidity (0..100%) +channel-type.shelly.motionActive.label = Motion Active +channel-type.shelly.motionActive.description = Indicates if motion sensor is active or within sleep time channel-type.shelly.motionTimestamp.label = Last Motion channel-type.shelly.motionTimestamp.description = Timestamp when last motion was detected. @@ -64,7 +68,6 @@ channel-type.shelly.rollerState.description = State of the roller (open/closed/s # LED disable channel-type.shelly.ledPowerDisable.label = Disable Power LED -channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be decativated +channel-type.shelly.ledPowerDisable.description = ON: The power status LED will be deactivated channel-type.shelly.ledStatusDisable.label = Disable Status LED -channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be decativated - +channel-type.shelly.ledStatusDisable.description = ON: The WiFi status LED will be deactivated diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly_de.properties b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly_de.properties index 71bbdacd2a68f..1275f1ba387c8 100644 --- a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly_de.properties +++ b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly_de.properties @@ -600,6 +600,8 @@ channel-type.shelly.sensorVibration.label = Vibration channel-type.shelly.sensorVibration.description = ON: Sensor hat eine Vibration erkannt channel-type.shelly.sensorMotion.label = Bewegung channel-type.shelly.sensorMotion.description = ON: Es wurde eine Bewegung erkannt +channel-type.shelly.motionActive.label = Bewegungssensor aktiv +channel-type.shelly.motionActive.description = Zeigt an, ob die Bewegungserkennung aktiv oder pausiert ist channel-type.shelly.motionTimestamp.label = Letzte Bewegung channel-type.shelly.motionTimestamp.description = Datum/Uhrzeit, wann die letzte Bewegung erkannt wurde. channel-type.shelly.sensorValve.label = Ventil @@ -689,5 +691,7 @@ channel-type.shelly.selfTest.state.option.not_completed = Nicht abgeschlossen channel-type.shelly.selfTest.state.option.running = Test läuft channel-type.shelly.selfTest.state.option.completed = abgeschlossen channel-type.shelly.selfTest.state.option.unknown = unbekannt +channel-type.shelly.sensorSleepTime.label = Sensor Standby Timer +channel-type.shelly.sensorSleepTime.description = Das Gerät sendet kein Ereignis solange die Zeitspanne nicht abgelaufen ist. diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/sensor.xml b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/sensor.xml index e0c8487f89931..9dee669ba2bcc 100644 --- a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/sensor.xml +++ b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/sensor.xml @@ -293,6 +293,13 @@ + + Switch + + channel-type.shelly.motionActive.description + + + Switch @@ -352,6 +359,12 @@ + + Number:Time + + @text/channel-type.shelly.sensorSleepTime.description + + String