]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] Improved Motion Support, Support CoIoT Unicast, fixes (#10220)
authorMarkus Michels <markus7017@gmail.com>
Wed, 3 Mar 2021 16:43:02 +0000 (17:43 +0100)
committerGitHub <noreply@github.com>
Wed, 3 Mar 2021 16:43:02 +0000 (17:43 +0100)
* New feature: Shelly Manager

Signed-off-by: Markus Michels <markus7017@gmail.com>
* Removed Shelly Manager to reduce PR size (will be another PR)

Signed-off-by: Markus Michels <markus7017@gmail.com>
* CoIoT initialization handles new COIOT options for the device,
sensorSleepTime is now adadvanced; Roller set position 0/100 is mapped
to UP/DOWN; Reference to Shelly Manager removed from README

Signed-off-by: Markus Michels <markus7017@gmail.com>
* Nullpointer check added on settings.coiot (4Pro has this null)

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

Signed-off-by: Markus Michels <markus7017@gmail.com>
* Use regex to extract fw version from string, check fw version to detect
restarted, README updated, moved channel sensorSleepTime from group
device to sensors

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

Signed-off-by: Markus Michels <markus7017@gmail.com>
24 files changed:
bundles/org.openhab.binding.shelly/README.md
bundles/org.openhab.binding.shelly/doc/images/manager/fwupgrade.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/doc/images/manager/overview.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/doc/images/manager/overview_actions.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/doc/images/manager/overview_devsettings.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/doc/images/manager/overview_devstatus.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyBindingConstants.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/ShellyHandlerFactory.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyDeviceStats.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyManagerInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyRelayHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyChannelDefinitions.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyTranslationProvider.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/util/ShellyUtils.java
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly_de.properties
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/sensor.xml

index 5750f1ff200fbf53f5e42ea82d0623cff110190e..dee49a1585f33a66cf60ac8769cffc50ed77d071 100644 (file)
@@ -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 (file)
index 0000000..0b50b67
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 (file)
index 0000000..5cf5f4b
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 (file)
index 0000000..70fdfd6
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 (file)
index 0000000..0be8e1b
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 (file)
index 0000000..f265710
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 (file)
index 0000000..c01a79f
Binary files /dev/null and b/bundles/org.openhab.binding.shelly/doc/images/manager/overview_versions.png differ
index e17a1c715765936fa5f1acd06d427a9aee696f74..adb8ab68d76e933f2d9d5d9c3bc9d05ec7765dd5 100755 (executable)
@@ -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
index 5708d30ca6f19d5a929ed40fdc30feba0e5e9c40..737094e72c7b620aba52daf46cfe6ed0d00c1d84 100755 (executable)
@@ -14,6 +14,7 @@ package org.openhab.binding.shelly.internal;
 
 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.coap.ShellyCoapServer;
 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 class ShellyHandlerFactory extends BaseThingHandlerFactory {
         return null;
     }
 
-    public Map<String, ShellyBaseHandler> getThingHandlers() {
-        return deviceListeners;
+    public Map<String, ShellyManagerInterface> getThingHandlers() {
+        return new HashMap<>(deviceListeners);
     }
 
     /**
index bb460bb32944feb8551f499214cc3244e8bbe174..d4be3688a9d26e2188606efcc68155fd33baab5b 100644 (file)
@@ -15,6 +15,7 @@ package org.openhab.binding.shelly.internal.api;
 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 class ShellyApiJsonDTO {
     }
 
     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 class ShellyApiJsonDTO {
     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 class ShellyApiJsonDTO {
         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 class ShellyApiJsonDTO {
         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 class ShellyApiJsonDTO {
         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 class ShellyApiJsonDTO {
         @SerializedName("favorites_enabled")
         public Boolean favoritesEnabled;
         public ArrayList<ShellyFavPos> 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 class ShellyApiJsonDTO {
         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 class ShellyApiJsonDTO {
         public Boolean discoverable; // FW 1.6+
         @SerializedName("cfg_changed_cnt")
         public Integer cfgChangedCount; // FW 1.8
+        @SerializedName("actions_stats")
+        public ShellyActionsStats astats;
 
         public ArrayList<ShellySettingsRelay> relays;
         public ArrayList<ShellySettingsRoller> rollers;
@@ -690,6 +723,9 @@ public class ShellyApiJsonDTO {
         public Long fsFree;
         public Long uptime;
 
+        @SerializedName("sleep_time") // Shelly Motion
+        public Integer sleepTime;
+
         public String json;
     }
 
@@ -908,6 +944,17 @@ public class ShellyApiJsonDTO {
             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 class ShellyApiJsonDTO {
 
         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<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device
index 74895c0e86ee5143176e9a44f3aeea135401bd70..2d4a832e6e316291da29e9bcb9a9f8a19c35c0eb 100644 (file)
@@ -18,8 +18,11 @@ import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 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 @@ import com.google.gson.Gson;
 @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 class ShellyDeviceProfile {
         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 class ShellyDeviceProfile {
         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 class ShellyDeviceProfile {
             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 class ShellyDeviceProfile {
         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 class ShellyDeviceProfile {
         }
         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;
+    }
 }
index 19ef1f1e05d82f330bb2a6519b7bb96adfb102b4..c57d9ef00a048eb7fc12d7a6fcf0f2f6ade43b8a 100644 (file)
@@ -194,26 +194,28 @@ public class ShellyHttpApi {
                     .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 class ShellyHttpApi {
                 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 class ShellyHttpApi {
         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
      *
index 8fed7e315a0803d04c3d1cc1c7e125fd2820cfc3..a239f41e0f2fa91a73d0c4e4f5b86248881424e7 100644 (file)
@@ -111,8 +111,12 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
                 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 class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
                 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
index db02cc289059a56a7f79a7ab6bfc12161f08a003..064621e57b51c13ae394ccb3e07cc9c60545fb47 100755 (executable)
@@ -38,6 +38,7 @@ import org.openhab.binding.shelly.internal.api.ShellyApiResult;
 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 @@ import org.slf4j.LoggerFactory;
  * @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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         // 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
             // 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                     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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                     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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     }
 
     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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
 
         // 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         } 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         }
     }
 
+    /**
+     * 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 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 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
      * @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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
      * @param status the /status result
      */
     protected void updateProperties(ShellyDeviceProfile profile, ShellySettingsStatus status) {
+        logger.debug("{}: Update properties", thingName);
         Map<String, Object> 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 class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
      * @return ShellyDeviceProfile instance
      * @throws ShellyApiException
      */
+    @Override
     public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException {
         try {
             refreshSettings |= forceRefresh;
@@ -1127,6 +1181,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         return profile;
     }
 
+    @Override
     public ShellyDeviceProfile getProfile() {
         return profile;
     }
@@ -1185,16 +1240,28 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         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<String, String> getStatsProp() {
-        return stats.asProperties(getString(profile.settings.timezone));
+    @Override
+    public ShellyHttpApi getApi() {
+        return api;
     }
 
-    public void resetStats() {
-        // reset statistics
-        stats = new ShellyDeviceStats();
+    public Map<String, String> getStatsProp() {
+        return stats.asProperties();
     }
 }
index 7fff96aae96c2869a7b72da4e3e3cd7695f824a0..fa0cc1333571210717cef836312ae0089a259a28 100644 (file)
@@ -64,6 +64,9 @@ public class ShellyComponents {
             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 class ShellyComponents {
                 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 class ShellyComponents {
         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 class ShellyComponents {
                         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);
index 7611aa9ffff204efd8ce7c12338ff0aa70031b32..002b37e458a3d05e7a61860a9847a48fa0770d1d 100644 (file)
@@ -26,7 +26,7 @@ import org.openhab.binding.shelly.internal.util.ShellyUtils;
 @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<String, String> asProperties(String timeZone) {
+    public Map<String, String> asProperties() {
         Map<String, String> 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 (file)
index 0000000..d1bb1a5
--- /dev/null
@@ -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);
+}
index e60878e9a55cc825f6fb3155a6c3f7608c695c5f..b11d6a840b6b41eebf1406ff3d44e656be1588ef 100644 (file)
@@ -134,11 +134,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));
+                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 class ShellyRelayHandler extends ShellyBaseHandler {
      */
     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 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
                 }
             }
 
-            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 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
                         "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 @@ 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));
             }
         }
@@ -399,6 +406,8 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
                     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,
index a4c4672e8daad9e60d0658a0bb7110d740c0e99e..e3e4308a3ae06d646b76b25855917b3917bd6634 100644 (file)
@@ -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 class ShellyChannelDefinitions {
                 .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 class ShellyChannelDefinitions {
             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 class ShellyChannelDefinitions {
         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 class ShellyChannelDefinitions {
                 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,
index b39225c99a3aad8c9c6bcd25fdbf9b7ad050ed91..ebc56a0a16889880284b199992b3b02887fed08d 100644 (file)
@@ -16,6 +16,7 @@ import java.util.Locale;
 
 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 class ShellyTranslationProvider {
         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;
         }
index 36b6a605e7f5ac1a19109fa81e2d7e4a6572effd..6f10ac16c0a030a906b9452f9dab619738ec019e 100644 (file)
@@ -24,6 +24,7 @@ import java.time.Instant;
 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 @@ import com.google.gson.internal.Primitives;
 @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> T fromJson(Gson gson, @Nullable String json, Class<T> classOfT) throws ShellyApiException {
         @Nullable
@@ -256,7 +258,7 @@ public class ShellyUtils {
     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 class ShellyUtils {
         }
     }
 
+    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;
index 3e2dd791a3262648e28236d3a5791a5abbf5fa59..e4b70f7aaa1cfd8a5d141b328f10738dc3b2a15b 100644 (file)
@@ -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
index 71bbdacd2a68f2e5b9564e4378506c2f08d4ce35..1275f1ba387c812c3b3fb75c74c53173e7829875 100644 (file)
@@ -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.
 
 
index e0c8487f89931f7ee9bcd6a2210c9b5f6d80bc44..9dee669ba2bcca3911b5e6110dd83bf29c194a6a 100644 (file)
                <state readOnly="true">
                </state>
        </channel-type>
+       <channel-type id="motionActive" advanced="true">
+               <item-type>Switch</item-type>
+               <label>@text/channel-type.shelly.motionActive.label</label>
+               <description>channel-type.shelly.motionActive.description</description>
+               <state readOnly="true">
+               </state>
+       </channel-type>
        <channel-type id="sensorMotion">
                <item-type>Switch</item-type>
                <label>Motion</label>
                <state readOnly="true">
                </state>
        </channel-type>
+       <channel-type id="sensorSleepTime" advanced="true">
+               <item-type>Number:Time</item-type>
+               <label>@text/channel-type.shelly.sensorSleepTime.label</label>
+               <description>@text/channel-type.shelly.sensorSleepTime.description</description>
+               <state readOnly="false" min="0" max="86400" pattern="%.0f %unit%"/>
+       </channel-type>
 
        <channel-type id="senseKey">
                <item-type>String</item-type>