]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] Add Shelly Motion, minor improvements (#10054)
authorMarkus Michels <markus7017@gmail.com>
Tue, 23 Feb 2021 08:48:13 +0000 (09:48 +0100)
committerGitHub <noreply@github.com>
Tue, 23 Feb 2021 08:48:13 +0000 (09:48 +0100)
* Support for Shelly Motion, some minotr improvements, README updated

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

Signed-off-by: Markus Michels <markus7017@gmail.com>
* Bug fixes from hardening

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

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

Signed-off-by: Markus Michels <markus7017@gmail.com>
* review changes, fix creations of sensors#motion and device#externalPower
for H%T; moved images/uiroller*.png to doc/images

Signed-off-by: Markus Michels <markus7017@gmail.com>
* missing in last fix

Signed-off-by: Markus Michels <markus7017@gmail.com>
31 files changed:
bundles/org.openhab.binding.shelly/README.md
bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png
bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png
bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png
bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png
bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png
bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png
bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png
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/ShellyCoIoTProtocol.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/coap/ShellyCoapHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapServer.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyBindingConfiguration.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.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 [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/util/ShellyChannelCache.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/device.xml
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/sensor.xml

index 9006b94dd3759eaee9d10e1169c7095ff11ca5d8..5750f1ff200fbf53f5e42ea82d0623cff110190e 100644 (file)
@@ -30,6 +30,7 @@ Refer to [Advanced Users](doc/AdvancedUsers.md) for more information on openHAB
 | shellydimmer       | Shelly Dimmer                                          | SHDM-1    |
 | shellydimmer2      | Shelly Dimmer2                                         | SHDM-2    |
 | shellyix3          | Shelly ix3                                             | SHIX3-1   |
+| shellyuni          | Shelly UNI                                             | SHUNI-1   |
 | shellyplug         | Shelly Plug                                            | SHPLG2-1  |
 | shellyplugs        | Shelly Plug-S                                          | SHPLG-S   |
 | shellyem           | Shelly EM with integrated Power Meters                 | SHEM      |
@@ -578,6 +579,23 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the
 |          |lastEvent    |String   |yes      |S/SS/SSS for 1/2/3x Shortpush or L for Longpush                        |
 |          |eventCount   |Number   |yes      |Counter gets incremented every time the device issues a button event.  |
 
+### Shelly UNI - Low voltage sensor/actor: shellyuni)
+
+|Group     |Channel      |Type     |read-only|Description                                                                 |
+|----------|-------------|---------|---------|----------------------------------------------------------------------------|
+|relay1    |             |         |         |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels       |
+|relay2    |             |         |         |See group relay1 for Shelly 2, no autoOn/autoOff/timerActive channels       |
+|sensors   |temperature1 |Number   |yes      |Temperature value of external sensor #1 (if connected to temp/hum addon)    |
+|          |temperature2 |Number   |yes      |Temperature value of external sensor #2 (if connected to temp/hum addon)    |
+|          |temperature3 |Number   |yes      |Temperature value of external sensor #3 (if connected to temp/hum addon)    |
+|          |humidity     |Number   |yes      |Humidity in percent (if connected to temp/hum addon)                        |
+|          |voltage      |Number   |yes      |ADCS voltage                                                                |
+|status    |input1       |Switch   |yes      |State of Input 1                                                            |
+|          |input2       |Switch   |yes      |State of Input 2                                                            |
+|          |button       |Trigger  |yes      |Event trigger, see section Button Events                                    |
+|          |lastEvent    |String   |yes      |S/SS/SSS for 1/2/3x Shortpush or L for Longpush                             |
+|          |eventCount   |Number   |yes      |Counter gets incremented every time the device issues a button event.       |
+
 ### Shelly Bulb (thing-type: shellybulb)
 
 |Group     |Channel      |Type     |read-only|Description                                                            |
index 8a2395c7767a991730128d247edec64ed898d6c5..0d09bdc7e65a3d40e161b9b69506c809f64e0ad3 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav1.png differ
index d80b49e7220cdddeb399ddd272d5e97282ee4019..af4ef7a5e597f970450fa8abd24fec88d439a01a 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_fav2.png differ
index 112b640965a844f062951605f1f5d751fec937ee..2dd54901f8f6194c4d37ec7c7bfcc5495b0d3456 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs1.png differ
index 5cb5a56cbb89271fbcab4e5ffb40d5ba51866c6a..c0b01c523f2201ed089f27214a1cca18c60fd66e 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs2.png differ
index 3c970c05f01cf5f1a26ade26ad3617199484d38b..fc9296d91620efd1b87cdf3f76697fc18f171ceb 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_obs3.png differ
index 628bfdb76eef0cb9d11f3d2ba6f299a25961c986..799d64c50db1038aec8c13763c528ac87e250f8c 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_rlogin.png differ
index c6432cdb973a9f31d98aa812a41637724342effe..07888025e2f8a39770a0f20ba78c6083baf61d51 100644 (file)
Binary files a/bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png and b/bundles/org.openhab.binding.shelly/doc/images/uiroller_wt.png differ
index c3bf59eaa0a13a8a6a34cf5a5410ed1c2fb29529..e17a1c715765936fa5f1acd06d427a9aee696f74 100755 (executable)
@@ -164,7 +164,7 @@ public class ShellyBindingConstants {
                     THING_TYPE_SHELLYVINTAGE, THING_TYPE_SHELLYDUORGBW, THING_TYPE_SHELLYRGBW2_COLOR,
                     THING_TYPE_SHELLYRGBW2_WHITE, THING_TYPE_SHELLYHT, THING_TYPE_SHELLYSENSE, THING_TYPE_SHELLYEYE,
                     THING_TYPE_SHELLYSMOKE, THING_TYPE_SHELLYGAS, THING_TYPE_SHELLYFLOOD, THING_TYPE_SHELLYDOORWIN,
-                    THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, /* THING_TYPE_SHELLMOTION, */
+                    THING_TYPE_SHELLYDOORWIN2, THING_TYPE_SHELLYBUTTON1, THING_TYPE_SHELLMOTION,
                     THING_TYPE_SHELLYPROTECTED, THING_TYPE_SHELLYUNKNOWN).collect(Collectors.toSet()));
 
     // Thing Configuration Properties
@@ -333,6 +333,7 @@ public class ShellyBindingConstants {
     public static final String ALARM_TYPE_OVERPOWER = "OVERPOWER";
     public static final String ALARM_TYPE_OVERLOAD = "OVERLOAD";
     public static final String ALARM_TYPE_LOADERR = "LOAD_ERROR";
+    public static final String ALARM_TYPE_SENSOR_ERROR = "SENSOR_ERROR";
     public static final String ALARM_TYPE_LOW_BATTERY = "LOW_BATTERY";
 
     // Event types
index 9e0d3c02b0e6cce617713f9b149b11a614d0b101..5708d30ca6f19d5a929ed40fdc30feba0e5e9c40 100755 (executable)
@@ -54,12 +54,14 @@ import io.reactivex.annotations.NonNull;
 @NonNullByDefault
 @Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly")
 public class ShellyHandlerFactory extends BaseThingHandlerFactory {
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
+
     private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
     private final HttpClient httpClient;
     private final ShellyTranslationProvider messages;
     private final ShellyCoapServer coapServer;
-    private final Set<ShellyBaseHandler> deviceListeners = ConcurrentHashMap.newKeySet();
-    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
+
+    private final Map<String, ShellyBaseHandler> deviceListeners = new ConcurrentHashMap<>();
     private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
     private String localIP = "";
     private int httpPort = -1;
@@ -129,7 +131,9 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
         }
 
         if (handler != null) {
-            deviceListeners.add(handler);
+            String uid = thing.getUID().getAsString();
+            deviceListeners.put(uid, handler);
+            logger.debug("Thing handler for uid {} added, total things = {}", uid, deviceListeners.size());
             return handler;
         }
 
@@ -137,13 +141,18 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
         return null;
     }
 
+    public Map<String, ShellyBaseHandler> getThingHandlers() {
+        return deviceListeners;
+    }
+
     /**
      * Remove handler of things.
      */
     @Override
     protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
         if (thingHandler instanceof ShellyBaseHandler) {
-            deviceListeners.remove(thingHandler);
+            String uid = thingHandler.getThing().getUID().getAsString();
+            deviceListeners.remove(uid);
         }
     }
 
@@ -158,8 +167,9 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
     public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType,
             Map<String, String> parameters) {
         logger.trace("{}: Dispatch event to thing handler", deviceName);
-        for (ShellyBaseHandler listener : deviceListeners) {
-            if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
+        for (Map.Entry<String, ShellyBaseHandler> listener : deviceListeners.entrySet()) {
+            ShellyBaseHandler thingHandler = listener.getValue();
+            if (thingHandler.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
                 // event processed
                 return;
             }
index 739861d62eaa713aadbf8e100abcd6a39f70529b..bb460bb32944feb8551f499214cc3244e8bbe174 100644 (file)
@@ -35,6 +35,7 @@ public class ShellyApiJsonDTO {
     public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud";
     public static final String SHELLY_URL_LIST_IR = "/ir/list";
     public static final String SHELLY_URL_SEND_IR = "/ir/emit";
+    public static final String SHELLY_URL_RESTART = "/reboot";
 
     public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay";
     public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay";
@@ -231,6 +232,11 @@ public class ShellyApiJsonDTO {
         public String hostname;
         public String fw;
         public Boolean auth;
+
+        @SerializedName("coiot") // Shelly Motion Multicast Endpoint
+        public String coiot;
+        public Integer longid;
+
         @SerializedName("num_outputs")
         public Integer numOutputs;
         @SerializedName("num_meters")
@@ -513,6 +519,8 @@ public class ShellyApiJsonDTO {
         public String newVersion;
         @SerializedName("old_version")
         public String oldVersion;
+        @SerializedName("beta_version")
+        public String betaVersion;
     }
 
     public static class ShellySettingsGlobal {
@@ -540,6 +548,8 @@ public class ShellyApiJsonDTO {
         ShellyStatusCloud cloud;
         @SerializedName("sleep_mode")
         public ShellySensorSleepMode sleepMode; // FW 1.6
+        @SerializedName("external_power")
+        public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
 
         public String timezone;
         public Double lat;
index 00cdb38fa12f9ccd4b1568578206b4fcb55d7e45..74895c0e86ee5143176e9a44f3aeea135401bd70 100644 (file)
@@ -81,6 +81,7 @@ public class ShellyDeviceProfile {
     public boolean isSensor = false; // true for HT & Smoke
     public boolean hasBattery = false; // true if battery device
     public boolean isSense = false; // true if thing is a Shelly Sense
+    public boolean isMotion = false; // true if thing is a Shelly Sense
     public boolean isHT = false; // true for H&T
     public boolean isDW = false; // true for Door Window sensor
     public boolean isButton = false; // true for a Shelly Button 1
@@ -91,6 +92,8 @@ public class ShellyDeviceProfile {
 
     public int updatePeriod = 2 * UPDATE_SETTINGS_INTERVAL_SECONDS + 10;
 
+    public String coiotEndpoint = "";
+
     public Map<String, String> irCodes = new HashMap<>(); // Sense: list of stored IR codes
 
     public ShellyDeviceProfile() {
@@ -188,13 +191,13 @@ public class ShellyDeviceProfile {
         boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
         boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
         boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
-        boolean isMotion = thingType.equals(THING_TYPE_SHELLYMOTION_STR);
         isHT = thingType.equals(THING_TYPE_SHELLYHT_STR);
         isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
+        isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
         isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
         isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_STR);
         isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR);
-        isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isSense;
+        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
     }
index 7a383f2a027cd0f880a4caf768e95017da3ac335..19ef1f1e05d82f330bb2a6519b7bb96adfb102b4 100644 (file)
@@ -36,7 +36,9 @@ import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyLis
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
+import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
 import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
@@ -228,6 +230,27 @@ public class ShellyHttpApi {
         request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
     }
 
+    public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
+    }
+
+    public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + user + "&password=" + password,
+                ShellySettingsLogin.class);
+    }
+
+    public String deviceReboot() throws ShellyApiException {
+        return callApi(SHELLY_URL_RESTART, String.class);
+    }
+
+    public String factoryReset() throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
+    }
+
+    public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
+        return callApi("/ota?" + uri, ShellySettingsUpdate.class);
+    }
+
     /**
      * Change between White and Color Mode
      *
index db9d7f614af1efeafcf4458c6175ad1f5f875d24..7161f413d13bf20b7588b0257395a91a6a21256c 100644 (file)
@@ -83,7 +83,7 @@ public class ShellyCoIoTProtocol {
         switch (sen.type.toLowerCase()) {
             case "b": // BatteryLevel +
                 updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
-                        toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
+                        toQuantityType(s.value, 0, Units.PERCENT));
                 break;
             case "h" /* Humidity */:
                 updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
index 78e2cc7f01e3baee2a6ebc3d8e67d7244b335ac6..8fed7e315a0803d04c3d1cc1c7e125fd2820cfc3 100644 (file)
@@ -297,8 +297,10 @@ public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCo
                 break;
             case "3119": // Motion timestamp
                 // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
-                        getTimestamp(getString(profile.settings.timezone), (long) s.value));
+                if (s.value != 0) {
+                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
+                            getTimestamp(getString(profile.settings.timezone), (long) s.value));
+                }
                 break;
             case "3120": // motionActive
                 // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
index 8b2db84103249ca00e67bdfa35e64a4385c25597..378c5730d2e029528ad8eb5a5a423f97277aad57 100644 (file)
@@ -81,7 +81,10 @@ public class ShellyCoapHandler implements ShellyCoapListener {
     private Request reqDescription = new Request(Code.GET, Type.CON);
     private Request reqStatus = new Request(Code.GET, Type.CON);
     private boolean discovering = false;
+    private int coiotPort = COIOT_PORT;
 
+    private long coiotMessages = 0;
+    private long coiotErrors = 0;
     private int lastSerial = -1;
     private String lastPayload = "";
     private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
@@ -118,8 +121,12 @@ public class ShellyCoapHandler implements ShellyCoapListener {
             }
 
             logger.debug("{}: Starting CoAP Listener", thingName);
-            coapServer.start(config.localIp, this);
-            statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS))
+            if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) {
+                String ps = substringAfter(profile.coiotEndpoint, ":");
+                coiotPort = Integer.parseInt(ps);
+            }
+            coapServer.start(config.localIp, coiotPort, this);
+            statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS))
                     .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
             @Nullable
             Endpoint endpoint = null;
@@ -152,10 +159,39 @@ public class ShellyCoapHandler implements ShellyCoapListener {
     @Override
     public void processResponse(@Nullable Response response) {
         if (response == null) {
+            coiotErrors++;
             return; // other device instance
         }
+        ResponseCode code = response.getCode();
+        if (code != ResponseCode.CONTENT) {
+            // error handling
+            logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
+                    response.getPayloadString());
+            coiotErrors++;
+            return;
+        }
+
+        List<Option> options = response.getOptions().asSortedList();
         String ip = response.getSourceContext().getPeerAddress().toString();
-        if (!ip.contains(config.deviceIp)) {
+        boolean match = ip.contains(config.deviceIp);
+        if (!match) {
+            // We can't identify device by IP, so we need to check the CoAP header's Global Device ID
+            for (Option opt : options) {
+                if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
+                    String devid = opt.getStringValue();
+                    if (devid.contains("#")) {
+                        // Format: <device type>#<mac address>#<coap version>
+                        String macid = substringBetween(devid, "#", "#");
+                        if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
+                            match = true;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        if (!match) {
+            // other instance
             return;
         }
 
@@ -164,93 +200,87 @@ public class ShellyCoapHandler implements ShellyCoapListener {
         String uri = "";
         int serial = -1;
         try {
+            coiotMessages++;
             if (logger.isDebugEnabled()) {
                 logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
                         response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
             }
             if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
                 logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
+                coiotErrors++;
                 return;
             }
 
-            if (response.getCode() == ResponseCode.CONTENT) {
-                payload = response.getPayloadString();
-                List<Option> options = response.getOptions().asSortedList();
-                int i = 0;
-                while (i < options.size()) {
-                    Option opt = options.get(i);
-                    switch (opt.getNumber()) {
-                        case OptionNumberRegistry.URI_PATH:
-                            uri = COLOIT_URI_BASE + opt.getStringValue();
-                            break;
-                        case COIOT_OPTION_GLOBAL_DEVID:
-                            devId = opt.getStringValue();
-                            String sVersion = substringAfterLast(devId, "#");
-                            int iVersion = Integer.parseInt(sVersion);
-                            if (coiotBound && (coiotVers != iVersion)) {
-                                logger.debug(
-                                        "{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
-                                        thingName, coiotVers, iVersion);
-                                thingHandler.reinitializeThing();
-                                coiotBound = false;
-                            }
-                            if (!coiotBound) {
-                                thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
-                                logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
-                                if (iVersion == COIOT_VERSION_1) {
-                                    coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
-                                } else if (iVersion == COIOT_VERSION_2) {
-                                    coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
-                                } else {
-                                    logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
-                                    return;
-                                }
-                                coiotVers = iVersion;
-                                coiotBound = true;
+            payload = response.getPayloadString();
+            for (Option opt : options) {
+                switch (opt.getNumber()) {
+                    case OptionNumberRegistry.URI_PATH:
+                        uri = COLOIT_URI_BASE + opt.getStringValue();
+                        break;
+                    case OptionNumberRegistry.URI_HOST: // ignore
+                        break;
+                    case OptionNumberRegistry.CONTENT_FORMAT: // ignore
+                        break;
+                    case COIOT_OPTION_GLOBAL_DEVID:
+                        devId = opt.getStringValue();
+                        String sVersion = substringAfterLast(devId, "#");
+                        int iVersion = Integer.parseInt(sVersion);
+                        if (coiotBound && (coiotVers != iVersion)) {
+                            logger.debug("{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
+                                    thingName, coiotVers, iVersion);
+                            thingHandler.reinitializeThing();
+                            coiotBound = false;
+                        }
+                        if (!coiotBound) {
+                            thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
+                            logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
+                            if (iVersion == COIOT_VERSION_1) {
+                                coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
+                            } else if (iVersion == COIOT_VERSION_2) {
+                                coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
+                            } else {
+                                logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
+                                return;
                             }
-                            break;
-                        case COIOT_OPTION_STATUS_VALIDITY:
-                            // validity = o.getIntegerValue();
-                            break;
-                        case COIOT_OPTION_STATUS_SERIAL:
-                            serial = opt.getIntegerValue();
-                            break;
-                        default:
-                            logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId,
-                                    opt.getNumber(), opt.getValue());
-                    }
-                    i++;
+                            coiotVers = iVersion;
+                            coiotBound = true;
+                        }
+                        break;
+                    case COIOT_OPTION_STATUS_VALIDITY:
+                        break;
+                    case COIOT_OPTION_STATUS_SERIAL:
+                        serial = opt.getIntegerValue();
+                        break;
+                    default:
+                        logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
+                                opt.getValue());
                 }
+            }
 
-                // If we received a CoAP message successful the thing must be online
-                thingHandler.setThingOnline();
+            // If we received a CoAP message successful the thing must be online
+            thingHandler.setThingOnline();
 
-                // The device changes the serial on every update, receiving a message with the same serial is a
-                // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
-                if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
-                        || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
-                    logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
-                    return;
-                }
+            // The device changes the serial on every update, receiving a message with the same serial is a
+            // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
+            if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
+                    || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
+                logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
+                return;
+            }
 
-                // fixed malformed JSON :-(
-                payload = fixJSON(payload);
+            // fixed malformed JSON :-(
+            payload = fixJSON(payload);
 
-                try {
-                    if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC)
-                            || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
-                        handleDeviceDescription(devId, payload);
-                    } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
-                            || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
-                        handleStatusUpdate(devId, payload, serial);
-                    }
-                } catch (ShellyApiException e) {
-                    logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
+            try {
+                if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
+                    handleDeviceDescription(devId, payload);
+                } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
+                        || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
+                    handleStatusUpdate(devId, payload, serial);
                 }
-            } else {
-                // error handling
-                logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, response.getCode(),
-                        response.getPayloadString());
+            } catch (ShellyApiException e) {
+                logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
+                coiotErrors++;
             }
 
             if (!discovering) {
@@ -261,6 +291,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
         } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
             logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
             resetSerial();
+            coiotErrors++;
         }
     }
 
@@ -541,7 +572,7 @@ public class ShellyCoapHandler implements ShellyCoapListener {
         }
 
         resetSerial();
-        return newRequest(ipAddress, uri, con).send();
+        return newRequest(ipAddress, coiotPort, uri, con).send();
     }
 
     /**
@@ -555,10 +586,10 @@ public class ShellyCoapHandler implements ShellyCoapListener {
      * @return new packet
      */
 
-    private Request newRequest(String ipAddress, String uri, Type con) {
+    private Request newRequest(String ipAddress, int port, String uri, Type con) {
         // We need to build our own Request to set an empty Token
         Request request = new Request(Code.GET, con);
-        request.setURI(completeUrl(ipAddress, uri));
+        request.setURI(completeUrl(ipAddress, port, uri));
         request.setToken(EMPTY_BYTE);
         request.addMessageObserver(new MessageObserverAdapter() {
             @Override
@@ -601,26 +632,37 @@ public class ShellyCoapHandler implements ShellyCoapListener {
         if (isStarted()) {
             logger.debug("{}: Stopping CoAP Listener", thingName);
             coapServer.stop(this);
-            if (statusClient != null) {
-                statusClient.shutdown();
+            CoapClient cclient = statusClient;
+            if (cclient != null) {
+                cclient.shutdown();
                 statusClient = null;
             }
-            if (!reqDescription.isCanceled()) {
-                reqDescription.cancel();
+            Request request = reqDescription;
+            if (!request.isCanceled()) {
+                request.cancel();
             }
-            if (!reqStatus.isCanceled()) {
-                reqStatus.cancel();
+            request = reqStatus;
+            if (!request.isCanceled()) {
+                request.cancel();
             }
         }
         resetSerial();
         coiotBound = false;
     }
 
+    public long getMessageCount() {
+        return coiotMessages;
+    }
+
+    public long getErrorCount() {
+        return coiotErrors;
+    }
+
     public void dispose() {
         stop();
     }
 
-    private static String completeUrl(String ipAddress, String uri) {
-        return "coap://" + ipAddress + ":" + COIOT_PORT + uri;
+    private static String completeUrl(String ipAddress, int port, String uri) {
+        return "coap://" + ipAddress + ":" + port + uri;
     }
 }
index c1cc0399d8cebba91a5f749a07dbbb3a20bca1f2..632562d3ccec4472cbdd07eb4fda05e78e46dd27 100644 (file)
@@ -49,7 +49,7 @@ public class ShellyCoapServer {
     boolean started = false;
     private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
     private @Nullable UdpMulticastConnector statusConnector;
-    private final CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);;
+    private CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);
     private final Set<ShellyCoapListener> coapListeners = ConcurrentHashMap.newKeySet();
 
     protected class ShellyStatusListener extends CoapResource {
@@ -68,6 +68,7 @@ public class ShellyCoapServer {
                 Code code = exchange.getRequest().getCode();
                 switch (code) {
                     case CUSTOM_30:
+                    case PUT: // Shelly Motion beta: incorrect, but handle the format
                         listener.processResponse(createResponse(request));
                         break;
                     default:
@@ -77,17 +78,18 @@ public class ShellyCoapServer {
         }
     }
 
-    public synchronized void start(String localIp, ShellyCoapListener listener)
+    public synchronized void start(String localIp, int port, ShellyCoapListener listener)
             throws UnknownHostException, SocketException {
         if (!started) {
-            logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, COIOT_PORT);
+            logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, port);
             NetworkConfig nc = NetworkConfig.getStandard();
             InetAddress localAddr = InetAddress.getByName(localIp);
-            InetSocketAddress localPort = new InetSocketAddress(COIOT_PORT);
+            InetSocketAddress localPort = new InetSocketAddress(port);
 
             // Join the multicast group on the selected network interface
             statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
             statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
+            server = new CoapServer(NetworkConfig.getStandard(), port);
             server.addEndpoint(statusEndpoint);
             CoapResource cit = new ShellyStatusListener("cit", this);
             CoapResource s = new ShellyStatusListener("s", this);
index 1d53b441e6095844d8187dffc4c98f09bd016cd1..e03a428c0c4405fc86b6eea8e054d6408e9bfe78 100644 (file)
@@ -20,6 +20,7 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 
 /**
  * The {@link ShellyBindingConfiguration} class contains fields mapping binding configuration parameters.
@@ -34,8 +35,8 @@ public class ShellyBindingConfiguration {
     public static final String CONFIG_LOCAL_IP = "localIP";
     public static final String CONFIG_AUTOCOIOT = "autoCoIoT";
 
-    public String defaultUserId = ""; // default for http basic user id
-    public String defaultPassword = ""; // default for http basic auth password
+    public String defaultUserId = "admin"; // default for http basic user id
+    public String defaultPassword = "admin"; // default for http basic auth password
     public String localIP = ""; // default:use OH network config
     public boolean autoCoIoT = true;
 
@@ -59,7 +60,10 @@ public class ShellyBindingConfiguration {
         }
     }
 
-    public void updateFromProperties(Dictionary<String, Object> properties) {
+    public void updateFromProperties(@Nullable Dictionary<String, Object> properties) {
+        if (properties == null) { // saw this once
+            return;
+        }
         List<String> keys = Collections.list(properties.keys());
         Map<String, Object> dictCopy = keys.stream().collect(Collectors.toMap(Function.identity(), properties::get));
         updateFromProperties(dictCopy);
index 81bc24158285abaa37ff01c5e9aa5800acb125eb..72c7148877201d4d2a6c11dd84b1891208ed0a0f 100755 (executable)
@@ -159,7 +159,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
                     // create shellyunknown thing - will be changed during thing initialization with valid credentials
                     thingUID = ShellyThingCreator.getThingUID(name, model, mode, true);
                 } else {
-                    logger.info("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
+                    logger.debug("{}: {}", name, messages.get("discovery.failed", address, e.toString()));
                 }
             } catch (IllegalArgumentException e) { // maybe some format description was buggy
                 logger.debug("{}: Discovery failed!", name, e);
index 67336c2b7e7c5e135e66d7b311cdc6b0302dc556..106f3d508afd4c0a54f49677e96aa55c34115f4f 100644 (file)
@@ -129,6 +129,10 @@ public class ShellyThingCreator {
         if (name.startsWith(THING_TYPE_SHELLYRGBW2_PREFIX)) {
             return mode.equals(SHELLY_MODE_COLOR) ? THING_TYPE_SHELLYRGBW2_COLOR_STR : THING_TYPE_SHELLYRGBW2_WHITE_STR;
         }
+        if (name.startsWith(THING_TYPE_SHELLYMOTION_STR)) {
+            // depending on firmware release the Motion advertises under shellymotion-xxx or shellymotionsensor-xxxx
+            return THING_TYPE_SHELLYMOTION_STR;
+        }
 
         // Check general mapping
         if (!deviceType.isEmpty()) {
index ef9983c38a68db10333158e9fbfa33d1ecf9a115..db02cc289059a56a7f79a7ab6bfc12161f08a003 100755 (executable)
@@ -83,6 +83,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     protected ShellyBindingConfiguration bindingConfig;
     protected ShellyThingConfiguration config = new ShellyThingConfiguration();
     protected ShellyDeviceProfile profile = new ShellyDeviceProfile(); // init empty profile to avoid NPE
+    protected ShellyDeviceStats stats = new ShellyDeviceStats();
     private final ShellyCoapHandler coap;
     public boolean autoCoIoT = false;
 
@@ -90,9 +91,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     protected boolean stopping = false;
     private boolean channelsCreated = false;
 
-    private long lastUptime = 0;
-    private long lastAlarmTs = 0;
-    private long lastTimeoutErros = -1;
     private long watchdog = now();
 
     private @Nullable ScheduledFuture<?> statusJob;
@@ -179,7 +177,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     }
 
     /**
-     * This routine is called every time the Thing configuration has been changed.
+     * This routine is called every time the Thing configuration has been changed
      */
     @Override
     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
@@ -218,7 +216,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.isSense) {
+        if (config.eventsCoIoT && profile.hasBattery && !profile.isMotion && !profile.isSense) {
             coap.start(thingName, config);
         }
 
@@ -240,6 +238,10 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
             setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "offline.conf-error-wrong-mode");
             return false;
         }
+        if (!getString(devInfo.coiot).isEmpty()) {
+            // New Shelly devices might use a different endpoint for the CoAP listener
+            tmpPrf.coiotEndpoint = devInfo.coiot;
+        }
 
         logger.debug("{}: Initializing device {}, type {}, Hardware: Rev: {}, batch {}; Firmware: {} / {} ({})",
                 thingName, tmpPrf.hostname, tmpPrf.deviceType, tmpPrf.hwRev, tmpPrf.hwBatchId, tmpPrf.fwVersion,
@@ -255,10 +257,10 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 tmpPrf.updatePeriod);
 
         // update thing properties
-        ShellySettingsStatus status = api.getStatus();
-        tmpPrf.updateFromStatus(status);
-        updateProperties(tmpPrf, status);
-        checkVersion(tmpPrf, status);
+        tmpPrf.status = api.getStatus();
+        tmpPrf.updateFromStatus(tmpPrf.status);
+        updateProperties(tmpPrf, tmpPrf.status);
+        checkVersion(tmpPrf, tmpPrf.status);
         if (autoCoIoT) {
             logger.debug("{}: Auto-CoIoT is enabled, disabling action urls", thingName);
             config.eventsCoIoT = true;
@@ -271,8 +273,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         }
 
         // All initialization done, so keep the profile and set Thing to ONLINE
-        profile = tmpPrf;
-        fillDeviceStatus(status, false);
+        fillDeviceStatus(tmpPrf.status, false);
         postEvent(ALARM_TYPE_NONE, false);
         api.setActionURLs(); // register event urls
         if (config.eventsCoIoT) {
@@ -281,6 +282,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         }
 
         logger.debug("{}: Thing successfully initialized.", thingName);
+        profile = tmpPrf;
         setThingOnline(); // if API call was successful the thing must be online
 
         return true; // success
@@ -370,6 +372,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
 
                 logger.trace("{}: Updating status", thingName);
                 ShellySettingsStatus status = api.getStatus();
+                profile.status = status;
                 profile.updateFromStatus(status);
 
                 // If status update was successful the thing must be online
@@ -379,6 +382,7 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME, getStringType(profile.settings.name));
                 updated |= this.updateDeviceStatus(status);
                 updated |= ShellyComponents.updateDeviceStatus(this, status);
+                fillDeviceStatus(status, updated);
                 updated |= updateInputs(status);
                 updated |= updateMeters(this, status);
                 updated |= updateSensors(this, status);
@@ -388,10 +392,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
 
                 // Restart watchdog when status update was successful (no exception)
                 restartWatchdog();
-
-                if (scheduledUpdates <= 1) {
-                    fillDeviceStatus(status, updated);
-                }
             }
         } catch (ShellyApiException e) {
             // http call failed: go offline except for battery devices, which might be in
@@ -402,7 +402,6 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                 if (!isWatchdogExpired()) {
                     logger.debug("{}: Ignore API Timeout, retry later", thingName);
                 } else {
-                    logger.debug("{}: Watchdog expired after {}sec,", thingName, profile.updatePeriod);
                     if (isThingOnline()) {
                         status = "offline.status-error-watchdog";
                     }
@@ -473,17 +472,15 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     }
 
     private boolean isWatchdogExpired() {
-        long timeout = profile.hasBattery ? profile.updatePeriod : profile.updatePeriod;
         long delta = now() - watchdog;
-        if ((watchdog > 0) && (delta > timeout)) {
-            logger.trace("{}: Watchdog expired after {}sec (started={}, now={}", thingName, delta, watchdog, now());
+        if ((watchdog > 0) && (delta > profile.updatePeriod)) {
+            stats.remainingWatchdog = delta;
             return true;
         }
         return false;
     }
 
     private boolean isWatchdogStarted() {
-        logger.trace("{}: Watchdog is {}", thingName, watchdog > 0 ? "started" : "inactive");
         return watchdog > 0;
     }
 
@@ -495,23 +492,26 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     private void fillDeviceStatus(ShellySettingsStatus status, boolean updated) {
         String alarm = "";
         boolean force = false;
-        Map<String, String> propertyUpdates = new TreeMap<>();
 
         // Update uptime and WiFi, internal temp
         ShellyComponents.updateDeviceStatus(this, status);
 
-        if (api.isInitialized() && (lastTimeoutErros != api.getTimeoutErrors())) {
-            propertyUpdates.put(PROPERTY_STATS_TIMEOUTS, String.valueOf(api.getTimeoutErrors()));
-            propertyUpdates.put(PROPERTY_STATS_TRECOVERED, String.valueOf(api.getTimeoutsRecovered()));
-            lastTimeoutErros = api.getTimeoutErrors();
+        if (api.isInitialized()) {
+            stats.timeoutErrors = api.getTimeoutErrors();
+            stats.timeoutsRecorvered = api.getTimeoutsRecovered();
         }
+        stats.remainingWatchdog = watchdog > 0 ? now() - watchdog : 0;
 
         // Check various device indicators like overheating
-        if ((status.uptime < lastUptime) && (profile.isInitialized()) && !profile.hasBattery) {
+        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);
+
             // Force re-initialization on next status update
-            if (!profile.hasBattery) {
+            if (!profile.hasBattery || profile.isMotion) {
                 reinitializeThing();
             }
         } else if (getBool(status.overtemperature)) {
@@ -521,15 +521,13 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         } else if (getBool(status.loaderror)) {
             alarm = ALARM_TYPE_LOADERR;
         }
-        lastUptime = getLong(status.uptime);
+        stats.lastUptime = getLong(status.uptime);
+        stats.coiotMessages = coap.getMessageCount();
+        stats.coiotErrors = coap.getErrorCount();
 
         if (!alarm.isEmpty()) {
             postEvent(alarm, force);
         }
-
-        if (!propertyUpdates.isEmpty()) {
-            flushProperties(propertyUpdates);
-        }
     }
 
     /**
@@ -542,14 +540,16 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
         State value = cache.getValue(channelId);
         String lastAlarm = value != UnDefType.NULL ? value.toString() : "";
 
-        if (force || !lastAlarm.equals(alarm) || (now() > (lastAlarmTs + HEALTH_CHECK_INTERVAL_SEC))) {
-            if (alarm.equals(ALARM_TYPE_NONE)) {
+        if (force || !lastAlarm.equals(alarm) || (now() > (stats.lastAlarmTs + HEALTH_CHECK_INTERVAL_SEC))) {
+            if (alarm.isEmpty() || alarm.equals(ALARM_TYPE_NONE)) {
                 cache.updateChannel(channelId, getStringType(alarm));
             } else {
                 logger.info("{}: {}", thingName, messages.get("event.triggered", alarm));
                 triggerChannel(channelId, alarm);
                 cache.updateChannel(channelId, getStringType(alarm));
-                lastAlarmTs = now();
+                stats.lastAlarm = alarm;
+                stats.lastAlarmTs = now();
+                stats.alarms++;
             }
         }
     }
@@ -757,7 +757,8 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
                             prf.fwId, SHELLY_API_MIN_FWVERSION));
                 }
             }
-            if (bindingConfig.autoCoIoT && (version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT) >= 0)) {
+            if (bindingConfig.autoCoIoT && ((version.compare(prf.fwVersion, SHELLY_API_MIN_FWCOIOT)) >= 0)
+                    || (prf.fwVersion.equalsIgnoreCase("production_test"))) {
                 if (!config.eventsCoIoT) {
                     logger.info("{}: {}", thingName, messages.get("versioncheck.autocoiot"));
                 }
@@ -1011,6 +1012,10 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
             properties.put(PROPERTY_SERVICE_NAME, hostname);
             logger.trace("{}: Updated serrviceName to {}", thingName, hostname);
         }
+        String deviceName = getString(profile.settings.name);
+        if (!deviceName.isEmpty()) {
+            properties.put(PROPERTY_DEV_NAME, deviceName);
+        }
 
         // add status properties
         if (status.wifiSta != null) {
@@ -1179,4 +1184,17 @@ public class ShellyBaseHandler extends BaseThingHandler implements ShellyDeviceL
     public boolean updateDeviceStatus(ShellySettingsStatus status) throws ShellyApiException {
         return false;
     }
+
+    public ShellyDeviceStats getStats() {
+        return stats;
+    }
+
+    public Map<String, String> getStatsProp() {
+        return stats.asProperties(getString(profile.settings.timezone));
+    }
+
+    public void resetStats() {
+        // reset statistics
+        stats = new ShellyDeviceStats();
+    }
 }
index e1b89986754d89fc3b431fa97d5dd03a25319e7d..7fff96aae96c2869a7b72da4e3e3cd7695f824a0 100644 (file)
@@ -30,6 +30,7 @@ import org.openhab.core.library.types.OpenClosedType;
 import org.openhab.core.library.unit.ImperialUnits;
 import org.openhab.core.library.unit.SIUnits;
 import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.UnDefType;
 
 /***
  * The{@link ShellyComponents} implements updates for supplemental components
@@ -56,7 +57,7 @@ public class ShellyComponents {
         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_UPTIME,
                 toQuantityType((double) getLong(status.uptime), DIGITS_NONE, Units.SECOND));
         thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_RSSI, mapSignalStrength(rssi));
-        if (status.tmp != null) {
+        if ((status.tmp != null) && !thingHandler.getProfile().isSensor) {
             thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
                     toQuantityType(getDouble(status.tmp.tC), DIGITS_NONE, SIUnits.CELSIUS));
         } else if (status.temperature != null) {
@@ -217,7 +218,7 @@ public class ShellyComponents {
                 }
             }
 
-            if (updated && !profile.isRoller && !profile.isRGBW2) {
+            if (!profile.isRoller && !profile.isRGBW2) {
                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUWATTS,
                         toQuantityType(accumulatedWatts, DIGITS_WATT, Units.WATT));
                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
@@ -244,7 +245,7 @@ public class ShellyComponents {
         ShellyDeviceProfile profile = thingHandler.getProfile();
 
         boolean updated = false;
-        if (profile.isSensor || profile.hasBattery || profile.isSense) {
+        if (profile.isSensor || profile.hasBattery) {
             ShellyStatusSensor sdata = thingHandler.api.getSensorStatus();
 
             if (!thingHandler.areChannelsCreated()) {
@@ -260,6 +261,7 @@ public class ShellyComponents {
                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_CONTACT,
                         getString(sdata.sensor.state).equalsIgnoreCase(SHELLY_API_DWSTATE_OPEN) ? OpenClosedType.OPEN
                                 : OpenClosedType.CLOSED);
+                String sensorError = getString(sdata.sensorError);
                 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR,
                         getStringType(sdata.sensorError));
                 if (changed) {
@@ -325,10 +327,21 @@ public class ShellyComponents {
                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VOLTAGE,
                         getDecimal(adc.voltage));
             }
+
+            boolean charger = (getInteger(profile.settings.externalPower) == 1) || getBool(sdata.charger);
+            if ((profile.settings.externalPower != null) || (sdata.charger != null)) {
+                updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
+                        charger ? OnOffType.ON : OnOffType.OFF);
+            }
             if (sdata.bat != null) { // no update for Sense
-                thingHandler.logger.trace("{}: Updating battery", thingHandler.thingName);
-                updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
-                        toQuantityType(getDouble(sdata.bat.value), DIGITS_PERCENT, Units.PERCENT));
+                // Shelly HT has external_power under settings, Sense and Motion charger under status
+                if (!charger || !profile.isHT) {
+                    updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
+                            toQuantityType(getDouble(sdata.bat.value), 0, Units.PERCENT));
+                } else {
+                    updated |= thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
+                            UnDefType.UNDEF);
+                }
                 boolean changed = thingHandler.updateChannel(CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LOW,
                         getDouble(sdata.bat.value) < thingHandler.config.lowBattery ? OnOffType.ON : OnOffType.OFF);
                 updated |= changed;
@@ -336,6 +349,7 @@ public class ShellyComponents {
                     thingHandler.postEvent(ALARM_TYPE_LOW_BATTERY, false);
                 }
             }
+
             if (sdata.motion != null) { // Shelly Sense
                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
                         getOnOff(sdata.motion));
@@ -343,15 +357,14 @@ public class ShellyComponents {
             if (sdata.sensor != null) { // Shelly Motion
                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
                         getOnOff(sdata.sensor.motion));
-                updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
-                        getTimestamp(getString(profile.settings.timezone), sdata.sensor.motionTimestamp));
+                long timestamp = getLong(sdata.sensor.motionTimestamp);
+                if (timestamp != 0) {
+                    updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
+                            getTimestamp(getString(profile.settings.timezone), timestamp));
+                }
                 updated |= thingHandler.updateChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
                         getOnOff(sdata.sensor.vibration));
             }
-            if (sdata.charger != null) {
-                updated |= thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
-                        getOnOff(sdata.charger));
-            }
 
             updated |= thingHandler.updateInputs(status);
 
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
new file mode 100644 (file)
index 0000000..7611aa9
--- /dev/null
@@ -0,0 +1,54 @@
+/**
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.shelly.internal.util.ShellyUtils;
+
+/***
+ * {@link ShellyDeviceStats} some statistical values for the thing
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class ShellyDeviceStats {
+    public long lastUptime = 0;
+    public long unexpectedRestarts = 0;
+    public long timeoutErrors = 0;
+    public long timeoutsRecorvered = 0;
+    public long remainingWatchdog = 0;
+    public long alarms = 0;
+    public String lastAlarm = "";
+    public long lastAlarmTs = 0;
+    public long coiotMessages = 0;
+    public long coiotErrors = 0;
+
+    public Map<String, String> asProperties(String timeZone) {
+        Map<String, String> prop = new HashMap<>();
+        prop.put("lastUptime", String.valueOf(lastUptime));
+        prop.put("unexpectedRestarts", String.valueOf(unexpectedRestarts));
+        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("coiotMessages", String.valueOf(coiotMessages));
+        prop.put("coiotErrors", String.valueOf(coiotErrors));
+        return prop;
+    }
+}
index c011644730e1bcaeb934fbc7f5a0032d4d14e8cd..e60878e9a55cc825f6fb3155a6c3f7608c695c5f 100644 (file)
@@ -235,7 +235,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
                 }
             }
 
-            if ((command == UpDownType.UP) || (command == OnOffType.ON)) {
+            if (command == UpDownType.UP || command == OnOffType.ON) {
                 logger.debug("{}: Open roller", thingName);
                 api.setRollerTurn(index, SHELLY_ALWD_ROLLER_TURN_OPEN);
                 int pos = profile.getRollerFav(config.favoriteUP - 1);
@@ -244,7 +244,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
                     logger.debug("{}: Use favoriteUP id {} for positioning roller({}%)", thingName, config.favoriteUP,
                             pos);
                 }
-            } else if ((command == UpDownType.DOWN) || (command == OnOffType.OFF)) {
+            } else if (command == UpDownType.DOWN || command == OnOffType.OFF) {
                 logger.debug("{}: Closing roller", thingName);
                 int pos = profile.getRollerFav(config.favoriteDOWN - 1);
                 if (pos > 0) {
index 0cce91ea6a4c7d131f90dc6a2192d897bcd927a7..a4c4672e8daad9e60d0658a0bb7110d740c0e99e 100644 (file)
@@ -410,9 +410,12 @@ public class ShellyChannelDefinitions {
                 CHANNEL_SENSOR_ILLUM);
         addChannel(thing, newChannels, sdata.flood != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
         addChannel(thing, newChannels, sdata.smoke != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD);
-        addChannel(thing, newChannels, sdata.charger != null, CHGR_DEVST, CHANNEL_DEVST_CHARGER);
-        addChannel(thing, newChannels, sdata.motion != null, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION);
-        if (sdata.sensor != null) { // DW2 or Motion
+        addChannel(thing, newChannels, (profile.settings.externalPower != null) || (sdata.charger != null), CHGR_DEVST,
+                CHANNEL_DEVST_CHARGER);
+        addChannel(thing, newChannels,
+                sdata.motion != null || ((sdata.sensor != null) && (sdata.sensor.motion != null)), CHANNEL_GROUP_SENSOR,
+                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.motionTimestamp != null, CHANNEL_GROUP_SENSOR, // Motion
                     CHANNEL_SENSOR_MOTION_TS);
index 6b573b9b8a00b424fb02268a626b750cb8b755f3..8f1e1d639d384ce2e0d75be1000420dd133c726f 100644 (file)
@@ -10,6 +10,7 @@
  *
  * SPDX-License-Identifier: EPL-2.0
  */
+
 package org.openhab.binding.shelly.internal.util;
 
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.mkChannelId;
index 5767d9b964a48fbec5f33a7824e8f45c8bbc09a3..36b6a605e7f5ac1a19109fa81e2d7e4a6572effd 100644 (file)
@@ -134,13 +134,14 @@ public class ShellyUtils {
     }
 
     public static String substringAfterLast(@Nullable String string, String pattern) {
-        if (string != null) {
-            int pos = string.lastIndexOf(pattern);
-            if (pos != -1) {
-                return string.substring(pos + pattern.length());
-            }
+        if (string == null) {
+            return "";
         }
-        return "";
+        int pos = string.lastIndexOf(pattern);
+        if (pos != -1) {
+            return string.substring(pos + pattern.length());
+        }
+        return string;
     }
 
     public static String substringBetween(@Nullable String string, String begin, String end) {
@@ -236,12 +237,11 @@ public class ShellyUtils {
         }
     }
 
-    public static String urlEncode(String input) throws ShellyApiException {
+    public static String urlEncode(String input) {
         try {
             return URLEncoder.encode(input, StandardCharsets.UTF_8.toString());
         } catch (UnsupportedEncodingException e) {
-            throw new ShellyApiException(
-                    "Unsupported encoding format: " + StandardCharsets.UTF_8.toString() + ", input=" + input, e);
+            return input;
         }
     }
 
index abc679655cc7a7246c2e5d4fb4916616d126283b..3e2dd791a3262648e28236d3a5791a5abbf5fa59 100644 (file)
@@ -8,7 +8,7 @@ binding.shelly.config.autoCoIoT.label = Auto-CoIoT
 binding.shelly.config.autoCoIoT.description = If enabled CoIoT will be automatically used when the devices runs a firmware version 1.6 or newer; false: Use thing configuration to enabled/disable CoIoT events.  
 
 discovery.failed# Config status messages
-config-status.error.missing-device-ip=IP address of the Shelly device is missing.
+config-status.error.missing-device-ip = IP address of the Shelly device is missing.
 
 # Thing status descriptions
 offline.conf-error-no-credentials = Device is password protected, but no credentials have been configured.
@@ -18,6 +18,7 @@ offline.status-error-timeout = Device is not reachable (API timeout).
 offline.status-error-unexpected-api-result = An unexpected API response. Please verify the logfile to get more detailed information.
 offline.status-error-watchdog = Device is not responding, seems to be unavailable.
 offline.status-error-restarted = The device has restarted and will be re-initialized.
+offline.status-error-fwupgrade = Firmware upgrade in progress
 
 message.versioncheck.failed = Unable to check firmware version: {0}
 message.versioncheck.beta = Device is running a Beta version: {0}/{1} ({2}),make sure this is newer than {3} release build.
index a3ed00cb4abc66177dddede89c1901c9f25f7554..71bbdacd2a68f2e5b9564e4378506c2f08d4ce35 100644 (file)
@@ -11,7 +11,7 @@ binding.shelly.config.autoCoIoT.label = Auto-CoIoT
 binding.shelly.config.autoCoIoT.description = Bei aktiviertem Auto-CoIoT wird das Protokoll aktiviert, sobald das Gerät eine Firmwareversion 1.6 oder neuer verwendet. Andernfalls wird dies über die Thing-Konfiguration gesteuert.  
 
 # Config status messages
-config-status.error.missing-deviceip=Die IP-Adresse des Shelly Gerätes ist nicht konfiguriert.
+config-status.error.missing-device-ip = Die IP-Adresse des Shelly Gerätes ist nicht konfiguriert.
 
 # Thing status descriptions
 offline.conf-error-no-credentials = Gerät ist passwortgeschützt, aber es sind keine Anmeldedaten konfiguriert.
@@ -21,6 +21,7 @@ offline.status-error-timeout = Das Ger
 offline.status-error-unexpected-api-result = Es trat ein unerwartetes Problem beim API-Zugriff auf. Überprüfen Sie die Logdatei für genauere Informationen.
 offline.status-error-watchdog = Das Gerät antwortet nicht und ist vermutlich nicht mehr verfügbar.
 offline.status-error-restarted = Das Gerät wurde neu gestartet und wird erneut initialisiert.
+offline.status-error-fwupgrade = Gerätesoftware wird aktualisiert
 
 # Status error messages
 config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfiguration
@@ -28,7 +29,7 @@ config-status.error.missing-userid = Keine Benutzerkennung in der Thing Konfigur
 # General messages
 message.versioncheck.failed = Firmware-Version konnte nicht geprüft werden: {0}
 message.versioncheck.beta = Es wurde eine Betaversion erkannt: {0}/{1} ({2}), bitte sicherstellen, dass diese neuer ist als Version {3} (Release Build).
-message.versioncheck.tooold = ACHTUNG: Eine alter Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
+message.versioncheck.tooold = ACHTUNG: Eine alte Firmware wurde erkannt: {0}/{1} ({2}), minimal erforderlich {3}.
 message.versioncheck.update = INFO: Eine neue Firmwareversion ist verfügbar, aktuell: {0}, neu: {1}
 message.versioncheck.autocoiot = INFO: Die Firmware unterstützt die Anforderung, Auto-CoIoT wurde aktiviert.
 message.init.noipaddress = Es konnte keine lokale IP-Adresse ermittelt werden. Bitte sicherstellen, dass IPv4 aktiviert ist und das richtige Interface in der openHAB Netzwerk-Konfiguration ausgewählt ist.
@@ -506,8 +507,8 @@ channel-type.shelly.whiteBrightness.label = Helligkeit
 channel-type.shelly.whiteBrightness.description = Helligkeit (0-100%, 0=aus)
 channel-type.shelly.meterWatts.label = Leistung
 channel-type.shelly.meterWatts.description = Aktueller Stromverbrauch in Watt
-channel-type.shelly.meterAccuWatts.label = Kumulierte Verbrauch
-channel-type.shelly.meterAccuWatts.description = Kumulierterr Verbrauch in Watt
+channel-type.shelly.meterAccuWatts.label = Kumulierter Verbrauch
+channel-type.shelly.meterAccuWatts.description = Kumulierter Verbrauch in Watt
 channel-type.shelly.meterAccuTotal.label = Kumulierter Gesamtverbrauch
 channel-type.shelly.meterAccuTotal.description = Kumulierter Gesamtverbrauch in kW/h
 channel-type.shelly.meterAccuReturned.label = Kumulierte Einspeisung
@@ -591,7 +592,7 @@ channel-type.shelly.sensorIllumination.state.option.unknown = Unbekannt
 channel-type.shelly.sensorIllumination.description = Angabe zum erkannten Tageslichtwert 
 channel-type.shelly.sensorPPM.label = Gas-Konzentration
 channel-type.shelly.sensorPPM.description = Gemessene Konzentration in PPM
-channel-type.shelly.sensorADC.label = Spannung (ADC)
+channel-type.shelly.sensorADC.label = Voltage (ADC)
 channel-type.shelly.sensorADC.description = Gemessene Spannung
 channel-type.shelly.sensorTilt.label = Öffnungswinkel
 channel-type.shelly.sensorTilt.description = Öffnungswinkel in Grad (erfordert Kalibrierung in der App)
index 3e2ba36c5d52d2536a55fd1089912b638f092419..2c3b71ed4171ed49072b3a137ec146e4c28363eb 100644 (file)
                <state readOnly="true">
                </state>
        </channel-type>
-       <channel-type id="batVoltage" advanced="true">
-               <item-type>Number:ElectricPotential</item-type>
-               <label>Battery Voltage</label>
-               <description>Battery voltage in V</description>
-               <state readOnly="true" pattern="%.1f %unit%">
+       <channel-type id="externalPower" advanced="true">
+               <item-type>Switch</item-type>
+               <label>External Power</label>
+               <description>ON: External power is connected</description>
+               <state readOnly="true">
                </state>
        </channel-type>
        <channel-type id="uptime" advanced="true">
index 172ae7e338cdcd18de13812ec8f320da538e688e..e0c8487f89931f7ee9bcd6a2210c9b5f6d80bc44 100644 (file)
        </thing-type>
 
        <thing-type id="shellymotion">
-               <label>Shelly Motion</label>
+               <label>Shelly Motion (SHMOS-1)</label>
                <description>Shelly Motion Sensor (battery powered)</description>
 
                <channel-groups>