]> git.basschouten.com Git - openhab-addons.git/commitdiff
[shelly] Add support for Shelly BLU series of devices (#15031)
authorMarkus Michels <markus7017@gmail.com>
Mon, 26 Jun 2023 14:37:50 +0000 (16:37 +0200)
committerGitHub <noreply@github.com>
Mon, 26 Jun 2023 14:37:50 +0000 (16:37 +0200)
* support for Pro 3EM (WIP)
* Support for Plug-S and Smoke added
* new channel resetTotals for emeters, new channel sensor#mute for Smoke
* Validate Temp reported by CoAP before updating channel, ignore 999
* Add support for Shelly BLU Button and Door/Window

Signed-off-by: Markus Michels <markus7017@gmail.com>
42 files changed:
bundles/org.openhab.binding.shelly/README.md
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/ShellyApiException.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiInterface.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/ShellyHttpClient.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTProtocol.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapListener.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiClient.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiJsonDTO.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2ApiRpc.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2RpcSocket.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/Shelly2RpctInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/ShellyBluApi.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/config/ShellyThingConfiguration.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java [new file with mode: 0644]
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/ShellyBluSensorHandler.java [new file with mode: 0644]
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/ShellyDeviceListener.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyLightHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyManagerInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingTable.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerCache.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerConstants.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerPage.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerServlet.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/provider/ShellyChannelDefinitions.java
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/addon/addon.xml
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/config2.xml
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/configblu.xml [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.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/shellyBlu_sensor.xml [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/resources/scripts/oh-blu-scanner.js [new file with mode: 0644]

index b00cb97aa37f42fa97c91ea8aa3fe1dbc9a443c3..07d6e97eddc1e3f4db3739542641e1bee2428dbe 100644 (file)
@@ -76,20 +76,20 @@ The binding provides the same feature set across all devices as good as possible
 
 ### Generation 2 Plus series
 
-| thing-type           | Model                                                    | Vendor ID                                     |
-| -------------------- | -------------------------------------------------------- | --------------------------------------------- |
-| shellyplus1          | Shelly Plus 1 with 1x relay                              | SNSW-001X16EU                                 |
-| shellyplus1pm        | Shelly Plus 1PM with 1x relay + power meter              | SNSW-001P16EU                                 |
-| shellyplus2pm-relay  | Shelly Plus 2PM with 2x relay + power meter, relay mode  | SNSW-002P16EU, SNSW-102P16EU                  |
-| shellyplus2pm-roller | Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU, SNSW-102P16EU                  |
-| shellyplusplug       | Shelly Plug-S                                            | SNPL-00112EU                                  |
-| shellyplusplug       | Shelly Plug-IT                                           | SNPL-00110IT                                  |
-| shellyplusplug       | Shelly Plug-UK                                           | SNPL-00112UK                                  |
-| shellyplusplug       | Shelly Plug-US                                           | SNPL-00116US                                  |
-| shellyplusi4         | Shelly Plus i4 with 4x AC input                          | SNSN-0024X                                    |
-| shellyplusi4dc       | Shelly Plus i4 with 4x DC input                          | SNSN-0D24X                                    |
-| shellyplusht         | Shelly Plus HT with temperature + humidity sensor        | SNSN-0013A                                    |
-| shellyplussmoke      | Shelly Plus Smoke sensor                                 | SNSN-0031Z                                    |
+| thing-type           | Model                                                    | Vendor ID                    |
+| -------------------- | -------------------------------------------------------- | ---------------------------- |
+| shellyplus1          | Shelly Plus 1 with 1x relay                              | SNSW-001X16EU                |
+| shellyplus1pm        | Shelly Plus 1PM with 1x relay + power meter              | SNSW-001P16EU                |
+| shellyplus2pm-relay  | Shelly Plus 2PM with 2x relay + power meter, relay mode  | SNSW-002P16EU, SNSW-102P16EU |
+| shellyplus2pm-roller | Shelly Plus 2PM with 2x relay + power meter, roller mode | SNSW-002P16EU, SNSW-102P16EU |
+| shellyplusplug       | Shelly Plug-S                                            | SNPL-00112EU                 |
+| shellyplusplug       | Shelly Plug-IT                                           | SNPL-00110IT                 |
+| shellyplusplug       | Shelly Plug-UK                                           | SNPL-00112UK                 |
+| shellyplusplug       | Shelly Plug-US                                           | SNPL-00116US                 |
+| shellyplusi4         | Shelly Plus i4 with 4x AC input                          | SNSN-0024X                   |
+| shellyplusi4dc       | Shelly Plus i4 with 4x DC input                          | SNSN-0D24X                   |
+| shellyplusht         | Shelly Plus HT with temperature + humidity sensor        | SNSN-0013A                   |
+| shellyplussmoke      | Shelly Plus Smoke sensor                                 | SNSN-0031Z                   |
 
 ### Generation 2 Pro series
 
@@ -104,6 +104,14 @@ The binding provides the same feature set across all devices as good as possible
 | shellypro3em        | Shelly Pro 3 with 3 integrated power meters              | SPEM-003CEBEU                                  |
 | shellypro4pm        | Shelly Pro 4 PM with 4x relay + power meter              | SPSW-004PE16EU, SPSW-104PE16EU                 |
 
+### Shelly BLU
+
+| thing-type        | Model                                                  | Vendor ID |
+| ----------------- | ------------------------------------------------------ | --------- |
+| shellyblubutton   | Shelly BLU Button 1                                    | SBBT      |
+| shellybludw       | Shelly BLU Door/Windows                                | SBDW      |
+
+
 ## Binding Configuration
 
 The binding has the following configuration options:
@@ -160,6 +168,29 @@ This allows routing the CoIoT/CoAP messages across multiple IP subnets without s
 You could use Shelly Manager (doc/ShellyManager.md) to easily do the setup (configuring the openHAB host as CoAP peer address).
 Keep Multicast mode if you have multiple hosts, which should receive the CoAP updates.
 
+### Discovery of BLU Devices
+
+The BLU devices use Bluetooth Low Energy (BLE).
+The binding can't communicate directly with the device, but the Plus/Pro series with firmware 0.14.1 or newer could be used as a gateway.
+The binding automatically installs a script on the Shelly Device (oh-blu-scanner), which forwards the BLU events to the binding using the WebSocket channel.
+
+Follow these steps to add the Shelly BLU Device to openHAB
+- Make sure a Shelly is near by the BLU device, enable Bluetooh on this device (the Bluetooth Gateway mode is not required)
+- Add this thing to openHAB, make sure thing gets online
+- Enable "BLU Gateway Support" in the thing configuration of the Shelly device acting as gateway.
+- Now press the button on your BLU device, this wakes up the device and the script forwards this event to the binding
+- As a result the corresponding thing should show up in the Inbox
+- Add the thing (at this point no channels are created), the new thing will show status CONFIG_PENDING
+- Click the device button again, the binding gets another event and creates the channels and thing changes status to ONLINE
+- Finally link the channels to the equipment in the model
+
+Note: During initialization the script 'oh-blu-scanner.js' gets installed and activated on the Shelly Gateway device.
+
+Every time an event is received sensors#lastUpdate and channels are updated with the reported values.
+device#wifiSignal indicates the Bluetooth signal strength and gets updated when the device sends an event.
+
+The binding supports multiple Shelly Plus/Pro as gateway devices unless they are added as thing and are ONLINE.
+
 ### Password Protected Devices
 
 The Shelly devices can be configured to require authorization through a user id and password.
@@ -251,6 +282,7 @@ You could also create a rule to catch those status changes or device alarms (see
 | eventsRoller       | true: register event "trigger" when the roller updates status | no        | true for roller devices                            |
 | favoriteUP         | 0-4: Favorite id for UP (see Roller Favorites)                | no        | 0 = no favorite id                                 |
 | favoriteDOWN       | 0-4: Favorite id for DOWN (see Roller Favorites)              | no        | 0 = no favorite id                                 |
+| enableBluGateway   | true: Active BLU gateway support (install script)             | no        | false                                              ]
 
 ### General Notes
 
@@ -380,7 +412,7 @@ A new alarm will be triggered on a new condition or every 5 minutes if the condi
 | TEMP_OVER  | Above "temperature over" threshold                                                               |
 | VIBRATION  | A vibration/tamper was detected (DW2 only)                                                       |
 
-Refer to section [Full Example](#full-example) for examples how to catch alarm triggers in openHAB rules
+Refer to section [Full Example](#full-example) for examples how to catch alarm triggers in openHAB rules.
 
 ## Channels
 
@@ -1341,6 +1373,38 @@ Channels lastEvent and eventCount are only available if input type is set to mom
 |        | timerActive | Switch  | yes       | Relay #1: ON: An auto-on/off timer is active                                      |
 |        | button      | Trigger | yes       | Event trigger, see section Button Events                                          |
 
+## Shelly BLU Devices
+
+### Shelly BLU Button 1 (thing-type: shellyblubutton)
+
+See notes on discovery of Shelly BLU devices above.
+
+| Group   | Channel       | Type     | read-only | Description                                                                         |
+| ------- | ------------- | -------- | --------- | ----------------------------------------------------------------------------------- |
+| status  | lastEvent     | String   | yes       | Last event type (S/SS/SSS/L)                                                        |
+|         | eventCount    | Number   | yes       | Counter gets incremented every time the device issues a button event.               |
+|         | button        | Trigger  | yes       | Event trigger with payload, see SHORT_PRESSED or LONG_PRESSED                       |
+|         | lastUpdate    | DateTime | yes       | Timestamp of the last measurement                                                   |
+| battery | batteryLevel  | Number   | yes       | Battery Level in %                                                                  |
+|         | lowBattery    | Switch   | yes       | Low battery alert (< 20%)                                                           |
+| device  | gatewayDevice | String   | yes       | Shelly forwarded last status update (BLU gateway), could vary from packet to packet |
+
+
+
+### Shelly BLU Door/Window Sensor (thing-type: shellybludw)
+
+See notes on discovery of Shelly BLU devices above.
+
+| Group   | Channel       | Type     | read-only | Description                                                                         |
+| ------- | ------------- | -------- | --------- | ----------------------------------------------------------------------------------- |
+| sensors | state         | Contact  | yes       | OPEN: Contact is open, CLOSED: Contact is closed                                    |
+|         | lux           | Number   | yes       | Brightness in Lux                                                                   |
+|         | tilt          | Number   | yes       | Tilt in Â° (angle), -1 indicates that the sensor is not calibrated                   |
+|         | 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%)                                                           |
+| device  | gatewayDevice | String   | yes       | Shelly forwarded last status update (BLU gateway), could vary from packet to packet |
+
 ## Full Example
 
 ### shelly.things
@@ -1582,4 +1646,3 @@ sitemap demo label="Home"
             Number   item=Shelly_Power
         }
 }
-```
index 57d68c68842849ec29ac486ac42d22096cf71bcb..6ef2c453f5cc09b96446fc70231e8495ec27c37d 100755 (executable)
@@ -85,11 +85,14 @@ public class ShellyBindingConstants {
             THING_TYPE_SHELLYPLUSSMOKE, //
             THING_TYPE_SHELLYPLUSPLUGS, //
             THING_TYPE_SHELLYPLUSPLUGUS, //
+            THING_TYPE_SHELLYBLUBUTTON, //
+            THING_TYPE_SHELLYBLUDW, //
             THING_TYPE_SHELLYPROTECTED, //
             THING_TYPE_SHELLYUNKNOWN);
 
     // Thing Configuration Properties
     public static final String CONFIG_DEVICEIP = "deviceIp";
+    public static final String CONFIG_DEVICEADDRESS = "deviceAddress";
     public static final String CONFIG_HTTP_USERID = "userId";
     public static final String CONFIG_HTTP_PASSWORD = "password";
     public static final String CONFIG_UPDATE_INTERVAL = "updateInterval";
@@ -99,6 +102,7 @@ public class ShellyBindingConstants {
     public static final String PROPERTY_DEV_TYPE = "deviceType";
     public static final String PROPERTY_DEV_MODE = "deviceMode";
     public static final String PROPERTY_DEV_GEN = "deviceGeneration";
+    public static final String PROPERTY_GW_DEVICE = "gatewayDevice";
     public static final String PROPERTY_HWREV = "deviceHwRev";
     public static final String PROPERTY_HWBATCH = "deviceHwBatch";
     public static final String PROPERTY_UPDATE_PERIOD = "devUpdatePeriod";
@@ -230,6 +234,7 @@ public class ShellyBindingConstants {
     // Device Status
     public static final String CHANNEL_GROUP_DEV_STATUS = "device";
     public static final String CHANNEL_DEVST_NAME = "deviceName";
+    public static final String CHANNEL_DEVST_GATEWAY = "gatewayDevice";
     public static final String CHANNEL_DEVST_UPTIME = "uptime";
     public static final String CHANNEL_DEVST_HEARTBEAT = "heartBeat";
     public static final String CHANNEL_DEVST_RSSI = "wifiSignal";
@@ -311,4 +316,7 @@ public class ShellyBindingConstants {
     public static final int UPDATE_SETTINGS_INTERVAL_SECONDS = 60; // check for updates every x sec
     public static final int HEALTH_CHECK_INTERVAL_SEC = 300; // Health check interval, 5min
     public static final int VIBRATION_FILTER_SEC = 5; // Absore duplicate vibration events for xx sec
+
+    public static final String BUNDLE_RESOURCE_SNIPLETS = "sniplets"; // where to find code sniplets in the bundle
+    public static final String BUNDLE_RESOURCE_SCRIPTS = "scripts"; // where to find scrips in the bundle
 }
index d5c4e32900f2e360d586a693375d3068ec12a8d8..08e00e2028a82c2024aa47e4e6de7088127a66c7 100755 (executable)
@@ -24,6 +24,7 @@ import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
+import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
 import org.openhab.binding.shelly.internal.handler.ShellyLightHandler;
 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
 import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler;
@@ -103,6 +104,11 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
         this.coapServer = new Shelly1CoapServer();
     }
 
+    @Activate
+    void activate() {
+        thingTable.startDiscoveryService(bundleContext);
+    }
+
     @Override
     public boolean supportsThingType(ThingTypeUID thingTypeUID) {
         return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
@@ -123,11 +129,15 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
                 || thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE_STR)
                 || thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE_STR) || thingType.equals(THING_TYPE_SHELLYDUORGBW_STR)
                 || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)) {
-            logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
+            logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
                     thingTypeUID.toString());
             handler = new ShellyLightHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
+        } else if (thingType.startsWith("shellyblu")) {
+            logger.debug("{}: Create new thing of type {} using ShellyBluSensorHandler", thing.getLabel(),
+                    thingTypeUID.toString());
+            handler = new ShellyBluSensorHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
         } else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
-            logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
+            logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
                     thingTypeUID.toString());
             handler = new ShellyRelayHandler(thing, messages, bindingConfig, thingTable, coapServer, httpClient);
         }
@@ -143,20 +153,13 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
         return null;
     }
 
-    public Map<String, ShellyManagerInterface> getThingHandlers() {
-        Map<String, ShellyManagerInterface> table = new HashMap<>();
-        for (Map.Entry<String, ShellyThingInterface> entry : thingTable.getTable().entrySet()) {
-            table.put(entry.getKey(), (ShellyManagerInterface) entry.getValue());
-        }
-        return table;
-    }
-
     /**
      * Remove handler of things.
      */
     @Override
     protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
         if (thingHandler instanceof ShellyBaseHandler) {
+            ((ShellyBaseHandler) thingHandler).stop();
             String uid = thingHandler.getThing().getUID().getAsString();
             thingTable.removeThing(uid);
         }
@@ -185,4 +188,12 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
     public ShellyBindingConfiguration getBindingConfig() {
         return bindingConfig;
     }
+
+    public Map<String, ShellyManagerInterface> getThingHandlers() {
+        Map<String, ShellyManagerInterface> table = new HashMap<>();
+        for (Map.Entry<String, ShellyThingInterface> entry : thingTable.getTable().entrySet()) {
+            table.put(entry.getKey(), (ShellyManagerInterface) entry.getValue());
+        }
+        return table;
+    }
 }
index 5fc4f5ad4c002827799d6f5d21aeadb6c5c2a7de..c562c7fdc1d94d2ed18fdc8c5f0f2a29f0f732f6 100644 (file)
@@ -83,6 +83,8 @@ public class ShellyApiException extends Exception {
                         string[1]);
             } else if (isMalformedURL()) {
                 message = "Invalid URL: " + url;
+            } else if (isJsonError()) {
+                message = getString(getMessage());
             } else if (isTimeout()) {
                 message = "API Timeout for " + url;
             } else if (!isConnectionError()) {
index f238ba501925bdf7ba1822d43382fec2482398e8..8b7716007d83ae37f713da7727f6160da5bc35a2 100644 (file)
@@ -46,7 +46,7 @@ public interface ShellyApiInterface {
 
     ShellySettingsStatus getStatus() throws ShellyApiException;
 
-    void setLedStatus(String ledName, Boolean value) throws ShellyApiException;
+    void setLedStatus(String ledName, boolean value) throws ShellyApiException;
 
     void setSleepTime(int value) throws ShellyApiException;
 
@@ -54,9 +54,9 @@ public interface ShellyApiInterface {
 
     void setRelayTurn(int id, String turnMode) throws ShellyApiException;
 
-    public void resetMeterTotal(int id) throws ShellyApiException;
+    void resetMeterTotal(int id) throws ShellyApiException;
 
-    public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
+    ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
 
     void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException;
 
@@ -80,7 +80,6 @@ public interface ShellyApiInterface {
 
     void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException;
 
-    // Valve
     void setValveMode(int id, boolean auto) throws ShellyApiException;
 
     void setValveTemperature(int valveId, int value) throws ShellyApiException;
@@ -137,5 +136,9 @@ public interface ShellyApiInterface {
 
     void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException;
 
+    void postEvent(String device, String index, String event, Map<String, String> parms) throws ShellyApiException;
+
     void close();
+
+    void startScan();
 }
index 5f96c5280e5571a6435376db76bda32fec231ef6..6230bbeb03532c35366ae9d681d7acc57bfe0261 100644 (file)
@@ -67,6 +67,8 @@ public class ShellyDeviceProfile {
     public boolean auth = false;
     public boolean alwaysOn = true;
     public boolean isGen2 = false;
+    public boolean isBlu = false;
+    public String gateway = "";
 
     public String hwRev = "";
     public String hwBatchId = "";
@@ -125,6 +127,10 @@ public class ShellyDeviceProfile {
             // Shelly UNI uses ext_temperature array, reformat to avoid GSON exception
             json = json.replace("ext_temperature", "ext_temperature_array");
         }
+        if (json.contains("\"ext_humidity\":{\"0\":[{")) {
+            // Shelly UNI uses ext_humidity array, reformat to avoid GSON exception
+            json = json.replace("ext_humidity", "ext_humidity_array");
+        }
         settingsJson = json;
         settings = fromJson(gson, json, ShellySettingsGlobal.class);
 
@@ -185,6 +191,8 @@ public class ShellyDeviceProfile {
             return;
         }
 
+        isBlu = thingType.startsWith("shellyblu"); // e.g. SBBT for BU Button
+
         isDimmer = deviceType.equalsIgnoreCase(SHELLYDT_DIMMER) || deviceType.equalsIgnoreCase(SHELLYDT_DIMMER2);
         isBulb = thingType.equals(THING_TYPE_SHELLYBULB_STR);
         isDuo = thingType.equals(THING_TYPE_SHELLYDUO_STR) || thingType.equals(THING_TYPE_SHELLYVINTAGE_STR)
@@ -201,12 +209,14 @@ public class ShellyDeviceProfile {
         boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
         boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
         isHT = thingType.equals(THING_TYPE_SHELLYHT_STR) || thingType.equals(THING_TYPE_SHELLYPLUSHT_STR);
-        isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR);
+        isDW = thingType.equals(THING_TYPE_SHELLYDOORWIN_STR) || thingType.equals(THING_TYPE_SHELLYDOORWIN2_STR)
+                || thingType.equals(THING_TYPE_SHELLYBLUDW_STR);
         isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
         isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
         isIX = thingType.equals(THING_TYPE_SHELLYIX3_STR) || thingType.equals(THING_TYPE_SHELLYPLUSI4_STR)
                 || thingType.equals(THING_TYPE_SHELLYPLUSI4DC_STR);
-        isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR) || thingType.equals(THING_TYPE_SHELLYBUTTON2_STR);
+        isButton = thingType.equals(THING_TYPE_SHELLYBUTTON1_STR) || thingType.equals(THING_TYPE_SHELLYBUTTON2_STR)
+                || thingType.equals(THING_TYPE_SHELLYBLUBUTTON_STR);
         isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense || isTRV;
         hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV;
         isTRV = thingType.equals(THING_TYPE_SHELLYTRV_STR);
@@ -241,7 +251,8 @@ public class ShellyDeviceProfile {
         } else if (hasRelays) {
             return numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + idx;
         } else if (isRGBW2) {
-            return settings.lights == null || settings.lights.size() <= 1 ? CHANNEL_GROUP_LIGHT_CONTROL
+            return settings.lights == null || settings.lights != null && settings.lights.size() <= 1
+                    ? CHANNEL_GROUP_LIGHT_CONTROL
                     : CHANNEL_GROUP_LIGHT_CHANNEL + idx;
         } else if (isLight) {
             return CHANNEL_GROUP_LIGHT_CONTROL;
index e9a34a4f0bca2dada27eac6d7d8ad879ed738782..8b97f9ae36330ba8c8f31a7dd4cd77d43df4e91f 100644 (file)
@@ -146,14 +146,15 @@ public class ShellyHttpClient {
                         HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
             }
             fillPostData(request, data);
-            logger.trace("{}: HTTP {} for {} {}", thingName, method, url, data);
+            logger.trace("{}: HTTP {} for {} {}\n{}", thingName, method, url, data, request.getHeaders());
 
             // Do request and get response
             ContentResponse contentResponse = request.send();
             apiResult = new ShellyApiResult(contentResponse);
             apiResult.httpCode = contentResponse.getStatus();
             String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
-            logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
+            logger.trace("{}: HTTP Response {}: {}\n{}", thingName, contentResponse.getStatus(), response,
+                    contentResponse.getHeaders());
 
             if (response.contains("\"error\":{")) { // Gen2
                 Shelly2RpcBaseMessage message = gson.fromJson(response, Shelly2RpcBaseMessage.class);
@@ -204,7 +205,7 @@ public class ShellyHttpClient {
             StringContentProvider postData;
             postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
             request.content(postData);
-            request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
+            // request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
         }
     }
 
@@ -253,4 +254,8 @@ public class ShellyHttpClient {
     public int getTimeoutsRecovered() {
         return timeoutsRecovered;
     }
+
+    public void postEvent(String device, String index, String event, Map<String, String> parms)
+            throws ShellyApiException {
+    }
 }
index 6c1f1ff8cdb2393fbb6d6a9967fc198aa55b4a4f..44f87beb044434ca1661c2c23ac414f2f28592ec 100644 (file)
@@ -739,7 +739,12 @@ public class Shelly1ApiJsonDTO {
         public ArrayList<ShellyRollerStatus> rollers;
         public ArrayList<ShellySettingsLight> lights;
         public ArrayList<ShellySettingsMeter> meters;
+
         public ArrayList<ShellySettingsEMeter> emeters;
+        public Double totalCurrent;
+        public Double totalPower;
+        public Double totalReturned;
+
         @SerializedName("ext_temperature")
         public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
         @SerializedName("ext_humidity")
index b8a2e5c441d480b93ebec9a8ce4a6ef1434228b8..e3bdab3e380513ecadff335b449b78cac27c0c88 100644 (file)
@@ -30,14 +30,14 @@ import org.openhab.core.types.State;
  */
 @NonNullByDefault
 public interface Shelly1CoIoTInterface {
-    int getVersion();
+    public int getVersion();
 
-    CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
+    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
 
-    void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
+    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
 
-    boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
             Map<String, State> updates, ShellyColorUtils col);
 
-    String getLastWakeup();
+    public String getLastWakeup();
 }
index 50f614ce7a9c4e57e7f5b255d4df33ffa1fb9c58..96032121727861faa8c4b9821275aa4ab9c4dce0 100644 (file)
@@ -210,8 +210,8 @@ public class Shelly1CoIoTProtocol {
                     "{}: Check button[{}] for event trigger (inButtonMode={}, isButton={}, hasBattery={}, serial={}, count={}, lastEventCount[{}]={}",
                     thingName, idx, profile.inButtonMode(idx), profile.isButton, profile.hasBattery, serial, count, idx,
                     lastEventCount[idx]);
-            if (profile.inButtonMode(idx) && ((profile.hasBattery && count == 1)
-                    || (lastEventCount[idx] != -1 && count != lastEventCount[idx]))) {
+            if (profile.inButtonMode(idx) && ((profile.hasBattery && count == 1) || lastEventCount[idx] == -1
+                    || count != lastEventCount[idx])) {
                 if (!profile.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
                     logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
                     thingHandler.triggerButton(group, idx, inputEvent[idx]);
index 68a4e9262f41f8736f39288a198fab8ceb911add..af71ad84493a6de3845ac640ef8f8113d73bcfcf 100644 (file)
@@ -176,7 +176,7 @@ public class Shelly1CoapHandler implements Shelly1CoapListener {
 
         List<Option> options = response.getOptions().asSortedList();
         String ip = response.getSourceContext().getPeerAddress().toString();
-        boolean match = 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) {
index eebaf02f65e3e54a169418414681542a828d67fe..49baceac373cc2e4fc60e25245f0d3cc8c3ef15b 100644 (file)
@@ -23,5 +23,5 @@ import org.eclipse.jdt.annotation.Nullable;
  */
 @NonNullByDefault
 public interface Shelly1CoapListener {
-    void processResponse(@Nullable Response response);
+    public void processResponse(@Nullable Response response);
 }
index 0972cfec81b98639e56cc00cc0b1284049558527..5160f9ac91a26731fcd4ff4efecb9fea369ae9d8 100644 (file)
@@ -177,7 +177,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
 
     @Override
     public void resetMeterTotal(int id) throws ShellyApiException {
-        callApi(SHELLY_URL_STATUS_EMETER + "/" + id + "/reset_totals=1", ShellyStatusRelay.class);
+        callApi(SHELLY_URL_STATUS_EMETER + "/" + id + "?reset_totals=true", ShellyStatusRelay.class);
     }
 
     @Override
@@ -245,7 +245,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
         } else if (profile.isLight) {
             type = SHELLY_CLASS_LIGHT;
         }
-        String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + (int) value;
+        String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
         httpRequest(uri);
     }
 
@@ -294,7 +294,7 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
     }
 
     @Override
-    public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
+    public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
         httpRequest(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
     }
 
@@ -752,4 +752,8 @@ public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterfa
     @Override
     public void close() {
     }
+
+    @Override
+    public void startScan() {
+    }
 }
index 7432f30e5c3a5309a4307d1476d374007ae32358..3736bc300742e5ddeeeb4977549710f454ca0cf1 100644 (file)
@@ -289,6 +289,16 @@ public class Shelly2ApiClient extends ShellyHttpClient {
             return false;
         }
 
+        if (em.totalCurrent != null) {
+            status.totalCurrent = em.totalCurrent;
+        }
+        if (em.totalActPower != null) {
+            status.totalPower = em.totalActPower;
+        }
+        if (em.totalAprtPower != null) {
+            status.totalReturned = em.totalAprtPower;
+        }
+
         ShellySettingsMeter sm = new ShellySettingsMeter();
         ShellySettingsEMeter emeter = status.emeters.get(0);
         sm.isValid = emeter.isValid = true;
@@ -683,7 +693,7 @@ public class Shelly2ApiClient extends ShellyHttpClient {
         throw new ShellyApiException("Thing/profile not initialized!");
     }
 
-    ShellyDeviceProfile getProfile() throws ShellyApiException {
+    protected ShellyDeviceProfile getProfile() throws ShellyApiException {
         if (thing != null) {
             return thing.getProfile();
         }
index e52ead4605473e6e2a2b1c2a5b52bf9c1430604c..58dcddfb7da090da2e2d8de9fe90a714dd14cc9c 100644 (file)
@@ -58,6 +58,15 @@ public class Shelly2ApiJsonDTO {
     public static final String SHELLYRPC_METHOD_WSSETCONFIG = "WS.SetConfig";
     public static final String SHELLYRPC_METHOD_SMOKE_SETCONFIG = "Smoke.SetConfig";
     public static final String SHELLYRPC_METHOD_SMOKE_MUTE = "Smoke.Mute";
+    public static final String SHELLYRPC_METHOD_SCRIPT_LIST = "Script.List";
+    public static final String SHELLYRPC_METHOD_SCRIPT_SETCONFIG = "Script.SetConfig";
+    public static final String SHELLYRPC_METHOD_SCRIPT_GETSTATUS = "Script.GetStatus";
+    public static final String SHELLYRPC_METHOD_SCRIPT_DELETE = "Script.Delete";
+    public static final String SHELLYRPC_METHOD_SCRIPT_CREATE = "Script.Create";
+    public static final String SHELLYRPC_METHOD_SCRIPT_GETCODE = "Script.GetCode";
+    public static final String SHELLYRPC_METHOD_SCRIPT_PUTCODE = "Script.PutCode";
+    public static final String SHELLYRPC_METHOD_SCRIPT_START = "Script.Start";
+    public static final String SHELLYRPC_METHOD_SCRIPT_STOP = "Script.Stop";
 
     public static final String SHELLYRPC_METHOD_NOTIFYSTATUS = "NotifyStatus"; // inbound status
     public static final String SHELLYRPC_METHOD_NOTIFYFULLSTATUS = "NotifyFullStatus"; // inbound status from bat device
@@ -118,6 +127,12 @@ public class Shelly2ApiJsonDTO {
     public static final String SHELLY2_EVENT_WIFICONNFAILED = "sta_connect_fail";
     public static final String SHELLY2_EVENT_WIFIDISCONNECTED = "sta_disconnected";
 
+    // BLU events
+    public static final String SHELLY2_BLU_GWSCRIPT = "oh-blu-scanner.js";
+    public static final String SHELLY2_EVENT_BLUPREFIX = "oh-blu.";
+    public static final String SHELLY2_EVENT_BLUSCAN = SHELLY2_EVENT_BLUPREFIX + "scan_result";
+    public static final String SHELLY2_EVENT_BLUDATA = SHELLY2_EVENT_BLUPREFIX + "data";
+
     // Error Codes
     public static final String SHELLY2_ERROR_OVERPOWER = "overpower";
     public static final String SHELLY2_ERROR_OVERTEMP = "overtemp";
@@ -549,6 +564,13 @@ public class Shelly2ApiJsonDTO {
 
                 @SerializedName("n_current")
                 public Double nCurrent;
+
+                @SerializedName("total_current")
+                public Double totalCurrent;
+                @SerializedName("total_act_power")
+                public Double totalActPower;
+                @SerializedName("total_aprt_power")
+                public Double totalAprtPower;
             }
 
             public static class Shelly2DeviceStatusEmData {
@@ -754,6 +776,9 @@ public class Shelly2ApiJsonDTO {
             // Cloud.SetConfig
             public Shelly2ConfigParms config;
 
+            // Script
+            public String name;
+
             public Shelly2RpcRequestParams withConfig() {
                 config = new Shelly2ConfigParms();
                 return this;
@@ -779,6 +804,11 @@ public class Shelly2ApiJsonDTO {
             params.pos = pos;
             return this;
         }
+
+        public Shelly2RpcRequest withName(String name) {
+            params.name = name;
+            return this;
+        }
     }
 
     public static class Shelly2WsConfigResponse {
@@ -793,6 +823,30 @@ public class Shelly2ApiJsonDTO {
         public Shelly2WsConfigResult result;
     }
 
+    public static class ShellyScriptListResponse {
+        public static class ShellyScriptListEntry {
+            public Integer id;
+            public String name;
+            public Boolean enable;
+            public Boolean running;
+        }
+
+        public ArrayList<ShellyScriptListEntry> scripts;
+    }
+
+    public static class ShellyScriptResponse {
+        public Integer id;
+        public Boolean running;
+        public Integer len;
+        public String data;
+    }
+
+    public static class ShellyScriptPutCodeParams {
+        public Integer id;
+        public String code;
+        public Boolean append;
+    }
+
     public static class Shelly2RpcBaseMessage {
         // Basic message format, e.g.
         // {"id":1,"src":"localweb528","method":"Shelly.GetConfig"}
@@ -804,8 +858,10 @@ public class Shelly2ApiJsonDTO {
         public Integer id;
         public String src;
         public String dst;
+        public String component;
         public String method;
         public Object params;
+        public String event;
         public Object result;
         public Shelly2AuthRequest auth;
         public Shelly2RpcMessageError error;
@@ -852,11 +908,49 @@ public class Shelly2ApiJsonDTO {
         public String algorithm;
     }
 
+    // BTHome samples
+    // BLU Button 1
+    // {"component":"script:2", "id":2, "event":"oh-blu.scan_result",
+    // "data":{"addr":"bc:02:6e:c3:a6:c7","rssi":-62,"tx_power":-128}, "ts":1682877414.21}
+    // {"component":"script:2", "id":2, "event":"oh-blu.data",
+    // "data":{"encryption":false,"BTHome_version":2,"pid":205,"Battery":100,"Button":1,"addr":"b4:35:22:fd:b3:81","rssi":-68},
+    // "ts":1682877399.22}
+    //
+    // BLU Door Window
+    // {"component":"script:2", "id":2, "event":"oh-blu.scan_result",
+    // "data":{"addr":"bc:02:6e:c3:a6:c7","rssi":-62,"tx_power":-128}, "ts":1682877414.21}
+    // {"component":"script:2", "id":2, "event":"oh-blu.data",
+    // "data":{"encryption":false,"BTHome_version":2,"pid":38,"Battery":100,"Illuminance":0,"Window":1,"Rotation":0,"addr":"bc:02:6e:c3:a6:c7","rssi":-62},
+    // "ts":1682877414.25}
+
+    public class Shelly2NotifyEventMessage {
+        public String addr;
+        public String name;
+        public Boolean encryption;
+        @SerializedName("BTHome_version")
+        public Integer bthVersion;
+        public Integer pid;
+        @SerializedName("Battery")
+        public Integer battery;
+        @SerializedName("Button")
+        public Integer buttonEvent;
+        @SerializedName("Illuminance")
+        public Integer illuminance;
+        @SerializedName("Window")
+        public Integer windowState;
+        @SerializedName("Rotation")
+        public Double rotation;
+
+        public Integer rssi;
+        public Integer tx_power;
+    }
+
     public class Shelly2NotifyEvent {
         public Integer id;
         public Double ts;
         public String component;
         public String event;
+        public Shelly2NotifyEventMessage data;
         public String msg;
         public Integer reason;
         @SerializedName("cfg_rev")
@@ -869,7 +963,8 @@ public class Shelly2ApiJsonDTO {
     }
 
     public static class Shelly2RpcNotifyEvent {
+        public String src;
         public Double ts;
-        Shelly2NotifyEventData params;
+        public Shelly2NotifyEventData params;
     }
 }
index e387019333efbe2a6d71ef16c807f5dc292cacac..a1d9f69f5cfbbf8f466ee3b81750a8e004d93188 100644 (file)
@@ -17,9 +17,15 @@ import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -65,6 +71,10 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequ
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcRequest.Shelly2RpcRequestParams;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2WsConfigResponse.Shelly2WsConfigResult;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptListResponse.ShellyScriptListEntry;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptPutCodeParams;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.ShellyScriptResponse;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
@@ -83,7 +93,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
     private final Logger logger = LoggerFactory.getLogger(Shelly2ApiRpc.class);
     private final @Nullable ShellyThingTable thingTable;
 
-    private boolean initialized = false;
+    protected boolean initialized = false;
     private boolean discovery = false;
     private Shelly2RpcSocket rpcSocket = new Shelly2RpcSocket();
     private Shelly2AuthResponse authInfo = new Shelly2AuthResponse();
@@ -139,6 +149,17 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         return initialized;
     }
 
+    @Override
+    public void startScan() {
+        if (config.enableBluGateway) {
+            try {
+                installScript(SHELLY2_BLU_GWSCRIPT);
+            } catch (ShellyApiException e) {
+            }
+        }
+    }
+
+    @SuppressWarnings("null")
     @Override
     public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
         ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
@@ -269,6 +290,19 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         if (!discovery) {
             getStatus(); // make sure profile.status is initialized (e.g,. relay/meter status)
             asyncApiRequest(SHELLYRPC_METHOD_GETSTATUS); // request periodic status updates from device
+
+            try {
+                logger.debug("{}: BLU Gateway support enabled for this device: {}", thingName, config.enableBluGateway);
+                if (config.enableBluGateway) {
+                    if (getBool(profile.settings.bluetooth)) {
+                        installScript(SHELLY2_BLU_GWSCRIPT);
+                    } else {
+                        logger.debug("{}: Bluetooth needs to be enabled to activate BLU Gateway mode", thingName);
+                    }
+                }
+            } catch (ShellyApiException e) {
+                logger.debug("{}: Device config failed", thingName, e);
+            }
         }
 
         return profile;
@@ -306,6 +340,117 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         }
     }
 
+    protected void installScript(String script) throws ShellyApiException {
+        String json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_LIST));
+        ShellyScriptListResponse scriptList = gson.fromJson(json, ShellyScriptListResponse.class);
+        Integer ourId = -1;
+        String code = "";
+
+        logger.debug("{}: Install or restart script {} on Shelly Device", thingName, script);
+        boolean running = false, upload = false;
+        if (scriptList != null) {
+            for (ShellyScriptListEntry s : scriptList.scripts) {
+                if (s.name.startsWith(script)) {
+                    ourId = s.id;
+                    running = s.running;
+                    logger.debug("{}: Script {} is already installed, id={}", thingName, script, ourId);
+                }
+            }
+        }
+
+        // get script code from bundle resources
+        String file = BUNDLE_RESOURCE_SCRIPTS + "/" + script;
+        ClassLoader cl = Shelly2ApiRpc.class.getClassLoader();
+        if (cl != null) {
+            try (InputStream inputStream = cl.getResourceAsStream(file)) {
+                if (inputStream != null) {
+                    code = new BufferedReader(new InputStreamReader(inputStream)).lines()
+                            .collect(Collectors.joining("\n"));
+                }
+            } catch (IOException | UncheckedIOException e) {
+                logger.debug("{}: Installation of script {} failed: Unable to read {} from bundle resources!",
+                        thingName, script, file, e);
+            }
+        }
+
+        boolean restart = false;
+        if (ourId == -1) {
+            // script not installed -> install it
+            upload = true;
+        } else {
+            try {
+                // verify that the same code version is active (avoid unnesesary flash updates)
+                json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_GETCODE).withId(ourId));
+                ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
+                if (!rsp.data.trim().equals(code.trim())) {
+                    logger.debug("{}: A script version was found, update to newest one", thingName);
+                    upload = true;
+                } else {
+                    logger.debug("{}: Same script version was found, restart", thingName);
+                    restart = true;
+                }
+            } catch (ShellyApiException e) {
+                logger.debug("{}: Unable to read current script code -> force update (deviced returned: {})", thingName,
+                        e.getMessage());
+                upload = true;
+            }
+        }
+
+        if (restart || (running && upload)) {
+            json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_STOP).withId(ourId));
+            // first stop running script
+            running = false;
+        }
+        if (upload && ourId != -1) {
+            // Delete existing script
+            logger.debug("{}: Delete existing script", thingName);
+            json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_DELETE).withId(ourId));
+        }
+
+        if (upload) {
+            logger.debug("{}: Script will be installed...", thingName);
+
+            // Create new script, get id
+            json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_CREATE).withName(script));
+            ShellyScriptResponse rsp = gson.fromJson(json, ShellyScriptResponse.class);
+            if (rsp != null) {
+                ourId = rsp.id;
+                logger.debug("{}: Script has been created, id={}", thingName, ourId);
+                upload = true;
+            }
+        }
+
+        if (upload) {
+            // Put script code for generated id
+            ShellyScriptPutCodeParams parms = new ShellyScriptPutCodeParams();
+            parms.id = ourId;
+            parms.append = false;
+            int length = code.length(), processed = 0, chunk = 1;
+            do {
+                int nextlen = Math.min(1024, length - processed);
+                parms.code = code.substring(processed, processed + nextlen);
+                logger.debug("{}: Uploading chunk {} of script (total {} chars, {} processed)", thingName, chunk,
+                        length, processed);
+                apiRequest(SHELLYRPC_METHOD_SCRIPT_PUTCODE, parms, String.class);
+                processed += nextlen;
+                chunk++;
+                parms.append = true;
+            } while (processed < length);
+            running = false;
+
+            Shelly2RpcRequestParams params = new Shelly2RpcRequestParams().withConfig();
+            params.config.enable = true;
+            apiRequest(SHELLYRPC_METHOD_SCRIPT_SETCONFIG, params, String.class);
+        }
+
+        if (!running) {
+            // Script was created or is there and stopped -> start it
+            json = apiRequest(new Shelly2RpcRequest().withMethod(SHELLYRPC_METHOD_SCRIPT_START).withId(ourId));
+            logger.debug("{}: Script {} was {} successful", thingName, script,
+                    restart ? "restarted" : "installed and started");
+        }
+    }
+
     @Override
     public void onConnect(String deviceIp, boolean connected) {
         if (thing == null && thingTable != null) {
@@ -755,7 +900,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
      * categories (e.g. bulbs)
      */
     @Override
-    public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
+    public void setLedStatus(String ledName, boolean value) throws ShellyApiException {
         throw new ShellyApiException("API call not implemented");
     }
 
@@ -866,6 +1011,7 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
         rpcSocket.sendMessage(gson.toJson(request)); // submit, result wull be async
     }
 
+    @SuppressWarnings("null")
     public <T> T apiRequest(String method, @Nullable Object params, Class<T> classOfT) throws ShellyApiException {
         String json = "";
         Shelly2RpcBaseMessage req = buildRequest(method, params);
@@ -905,8 +1051,18 @@ public class Shelly2ApiRpc extends Shelly2ApiClient implements ShellyApiInterfac
                 throw e;
             }
         }
-        json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
-        return fromJson(gson, json, classOfT);
+        Shelly2RpcBaseMessage response = gson.fromJson(json, Shelly2RpcBaseMessage.class);
+        if (response == null) {
+            throw new IllegalArgumentException("Unable to cover API result to obhect");
+        }
+        if (response.result != null) {
+            // return sub element result as requested class type
+            json = gson.toJson(gson.fromJson(json, Shelly2RpcBaseMessage.class).result);
+            return fromJson(gson, json, classOfT);
+        } else {
+            // return direct format
+            return gson.fromJson(json, classOfT);
+        }
     }
 
     public <T> T apiRequest(Shelly2RpcRequest request, Class<T> classOfT) throws ShellyApiException {
index c64684e6b53dc5321c00566bb68b616c9d8879e6..ecf724ff0801e2ecd23cc8871b0c840cb25390c6 100644 (file)
@@ -34,9 +34,11 @@ import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
 import org.eclipse.jetty.websocket.client.WebSocketClient;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcBaseMessage;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyStatus;
+import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
 import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
 import org.slf4j.Logger;
@@ -250,8 +252,34 @@ public class Shelly2RpcSocket {
                         handler.onNotifyStatus(status);
                         return;
                     case SHELLYRPC_METHOD_NOTIFYEVENT:
-                        handler.onNotifyEvent(fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
-                        return;
+                        Shelly2RpcNotifyEvent events = fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class);
+                        events.src = message.src;
+                        if (events.params == null || events.params.events == null) {
+                            logger.debug("{}: Malformed event data: {}", thingName, receivedMessage);
+                        } else {
+                            for (Shelly2NotifyEvent e : events.params.events) {
+                                if (getString(e.event).startsWith(SHELLY2_EVENT_BLUPREFIX)) {
+                                    String address = getString(e.data.addr).replaceAll(":", "");
+                                    if (thingTable != null && thingTable.findThing(address) != null) {
+                                        if (thingTable != null) { // known device
+                                            ShellyThingInterface thing = thingTable.getThing(address);
+                                            Shelly2ApiRpc api = (Shelly2ApiRpc) thing.getApi();
+                                            handler = api.getRpcHandler();
+                                            handler.onNotifyEvent(
+                                                    fromJson(gson, receivedMessage, Shelly2RpcNotifyEvent.class));
+                                        }
+                                    } else { // new device
+                                        if (e.event.equals(SHELLY2_EVENT_BLUSCAN)) {
+                                            ShellyBluSensorHandler.addBluThing(message.src, e, thingTable);
+                                        } else {
+                                            logger.debug("{}: NotifyEvent {} for unknown device {}", message.src,
+                                                    e.event, e.data.name);
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        break;
                     default:
                         handler.onMessage(receivedMessage);
                 }
@@ -259,7 +287,9 @@ public class Shelly2RpcSocket {
                 logger.debug("{}: No Rpc listener registered for device {}, skip message: {}", thingName,
                         getString(message.src), receivedMessage);
             }
-        } catch (ShellyApiException | IllegalArgumentException | NullPointerException e) {
+        } catch (ShellyApiException | IllegalArgumentException e) {
+            logger.debug("{}: Unable to process Rpc message ({}): {}", thingName, e.getMessage(), receivedMessage);
+        } catch (NullPointerException e) {
             logger.debug("{}: Unable to process Rpc message: {}", thingName, receivedMessage, e);
         }
     }
index 4387db1413cd623a00d787f8127aca619699dcd9..82ffdee92cb2f081cd84d143c23712ce8a08728c 100644 (file)
@@ -24,15 +24,15 @@ import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNoti
 @NonNullByDefault
 public interface Shelly2RpctInterface {
 
-    void onConnect(String deviceIp, boolean connected);
+    public void onConnect(String deviceIp, boolean connected);
 
-    void onMessage(String decodedmessage);
+    public void onMessage(String decodedmessage);
 
-    void onNotifyStatus(Shelly2RpcNotifyStatus message);
+    public void onNotifyStatus(Shelly2RpcNotifyStatus message);
 
-    void onNotifyEvent(Shelly2RpcNotifyEvent message);
+    public void onNotifyEvent(Shelly2RpcNotifyEvent message);
 
-    void onClose(int statusCode, String reason);
+    public void onClose(int statusCode, String reason);
 
-    void onError(Throwable cause);
+    public void onError(Throwable cause);
 }
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/ShellyBluApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api2/ShellyBluApi.java
new file mode 100644 (file)
index 0000000..7d35d05
--- /dev/null
@@ -0,0 +1,323 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api2;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySensorSleepMode;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorAccel;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorBat;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorLux;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellySensorState;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2RpcNotifyEvent;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyBluSensorHandler;
+import org.openhab.binding.shelly.internal.handler.ShellyComponents;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ShellyBluApi} implementsBLU interface
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class ShellyBluApi extends Shelly2ApiRpc {
+    private static final Logger logger = LoggerFactory.getLogger(ShellyBluApi.class);
+    private boolean connected = false; // true = BLU devices has connected
+    private ShellySettingsStatus deviceStatus = new ShellySettingsStatus();
+    private int lastPid = -1;
+
+    private static final Map<String, String> MAP_INPUT_EVENT_TYPE = new HashMap<>();
+    static {
+        MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_1PUSH, SHELLY_BTNEVENT_1SHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_2PUSH, SHELLY_BTNEVENT_2SHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_3PUSH, SHELLY_BTNEVENT_3SHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LPUSH, SHELLY_BTNEVENT_LONGPUSH);
+        MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_LSPUSH, SHELLY_BTNEVENT_LONGSHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put(SHELLY2_EVENT_SLPUSH, SHELLY_BTNEVENT_SHORTLONGPUSH);
+        MAP_INPUT_EVENT_TYPE.put("1", SHELLY_BTNEVENT_1SHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put("2", SHELLY_BTNEVENT_2SHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put("3", SHELLY_BTNEVENT_3SHORTPUSH);
+        MAP_INPUT_EVENT_TYPE.put("4", SHELLY_BTNEVENT_LONGPUSH);
+    }
+
+    /**
+     * Regular constructor - called by Thing handler
+     *
+     * @param thingName Symbolic thing name
+     * @param thing Thing Handler (ThingHandlerInterface)
+     */
+    public ShellyBluApi(String thingName, ShellyThingTable thingTable, ShellyThingInterface thing) {
+        super(thingName, thingTable, thing);
+
+        ShellyInputState input = new ShellyInputState();
+        deviceStatus.inputs = new ArrayList<>();
+        input.input = 0;
+        input.event = "";
+        input.eventCount = 0;
+        deviceStatus.inputs.add(input);
+    }
+
+    @Override
+    public void initialize() throws ShellyApiException {
+        if (!initialized) {
+            initialized = true;
+            connected = false;
+        } else {
+        }
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    @Override
+    public void setConfig(String thingName, ShellyThingConfiguration config) {
+        this.thingName = thingName;
+        this.config = config;
+    }
+
+    @Override
+    public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
+        ShellySettingsDevice info = new ShellySettingsDevice();
+        info.hostname = !config.serviceName.isEmpty() ? config.serviceName : "";
+        info.fw = "1234";
+        info.type = "SBBT";
+        info.mac = config.deviceAddress;
+        info.auth = false;
+        info.gen = 99;
+        return info;
+    }
+
+    @Override
+    public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+        ShellyDeviceProfile profile = thing != null ? getProfile() : new ShellyDeviceProfile();
+
+        profile.isBlu = true;
+        profile.settingsJson = "{}";
+        profile.thingName = thingName;
+        profile.name = getString(profile.settings.name);
+        if (profile.gateway.isEmpty()) {
+            profile.gateway = getThing().getProperty(PROPERTY_GW_DEVICE);
+        }
+
+        ShellySettingsDevice device = getDeviceInfo();
+        profile.settings.device = device;
+        profile.hostname = device.hostname;
+        profile.deviceType = device.type;
+        profile.mac = device.mac;
+        profile.auth = device.auth;
+        if (config.serviceName.isEmpty()) {
+            config.serviceName = getString(profile.hostname);
+        }
+        profile.fwDate = substringBefore(device.fw, "/");
+        profile.fwVersion = substringBefore(ShellyDeviceProfile.extractFwVersion(device.fw.replace("/", "/v")), "-");
+        profile.status.update.oldVersion = profile.fwVersion;
+        profile.status.hasUpdate = profile.status.update.hasUpdate = false;
+
+        if (profile.hasBattery) {
+            profile.settings.sleepMode = new ShellySensorSleepMode();
+            profile.settings.sleepMode.unit = "m";
+            profile.settings.sleepMode.period = 720;
+        }
+
+        if (profile.isButton) {
+            ShellySettingsInput settings = new ShellySettingsInput();
+            profile.numInputs = 1;
+            settings.btnType = SHELLY_BTNT_MOMENTARY;
+
+            if (profile.settings.inputs != null) {
+                profile.settings.inputs.set(0, settings);
+            } else {
+                profile.settings.inputs = new ArrayList<>();
+                profile.settings.inputs.add(settings);
+            }
+            profile.status = deviceStatus;
+        }
+
+        if (!connected) {
+            throw new ShellyApiException("BLU Device not yet connected");
+        }
+
+        profile.initialized = true;
+        return profile;
+    }
+
+    @Override
+    public ShellySettingsStatus getStatus() throws ShellyApiException {
+        if (!connected) {
+            throw new ShellyApiException("Thing is not yet initialized -> status not available");
+        }
+        return deviceStatus;
+    }
+
+    @Override
+    public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
+        if (!connected) {
+            throw new ShellyApiException("Thing is not yet initialized -> sensor data not available");
+        }
+
+        return sensorData;
+    }
+
+    @Override
+    public void onNotifyEvent(Shelly2RpcNotifyEvent message) {
+        logger.trace("{}: ShellyEvent received: {}", thingName, gson.toJson(message));
+
+        boolean updated = false;
+        ShellyBluSensorHandler t = (ShellyBluSensorHandler) thing;
+        if (t == null) {
+            logger.debug("{}: Thing is not initialized -> ignore event", thingName);
+            return;
+        }
+
+        try {
+            ShellyDeviceProfile profile = getProfile();
+
+            t.incProtMessages();
+
+            if (!connected) {
+                connected = true;
+                t.setThingOnline();
+            } else {
+                t.restartWatchdog();
+            }
+
+            for (Shelly2NotifyEvent e : message.params.events) {
+                logger.debug("{}: BluEvent received: {}", thingName, gson.toJson(message));
+                String event = getString(e.event);
+                if (event.startsWith(SHELLY2_EVENT_BLUPREFIX)) {
+                    logger.debug("{}: BLU event {} received from address {}, pid={}", thingName, event,
+                            getString(e.data.addr), getInteger(e.data.pid));
+                    if (e.data.pid != null) {
+                        int pid = e.data.pid;
+                        if (pid == lastPid) {
+                            logger.debug("{}: Duplicate packet for PID={} received, ignore", thingName, pid);
+                            break;
+                        }
+                        lastPid = pid;
+                    }
+                    getThing().getProfile().gateway = message.src;
+                }
+
+                switch (event) {
+                    case SHELLY2_EVENT_BLUSCAN:
+                        if (e.data == null || e.data.addr == null) {
+                            logger.debug("{}: Inconsistent BLU scan result ignored: {}", thingName,
+                                    gson.toJson(message));
+                            break;
+                        }
+                        logger.debug("{}: BLU Device discovered", thingName);
+                        if (e.data.name != null) {
+                            profile.settings.name = buildBluServiceName(e.data.name, e.data.addr);
+                        }
+                        break;
+                    case SHELLY2_EVENT_BLUDATA:
+                        if (e.data == null || e.data.addr == null || e.data.pid == null) {
+                            logger.debug("{}: Inconsistent BLU packet ignored: {}", thingName, gson.toJson(message));
+                            break;
+                        }
+
+                        if (e.data.battery != null) {
+                            if (sensorData.bat == null) {
+                                sensorData.bat = new ShellySensorBat();
+                            }
+                            sensorData.bat.value = (double) e.data.battery;
+                        }
+                        if (e.data.rssi != null) {
+                            deviceStatus.wifiSta.rssi = e.data.rssi;
+                        }
+                        if (e.data.windowState != null) {
+                            if (sensorData.sensor == null) {
+                                sensorData.sensor = new ShellySensorState();
+                            }
+                            sensorData.sensor.isValid = true;
+                            sensorData.sensor.state = e.data.windowState == 1 ? SHELLY_API_DWSTATE_OPEN
+                                    : SHELLY_API_DWSTATE_CLOSE;
+                        }
+                        if (e.data.illuminance != null) {
+                            if (sensorData.lux == null) {
+                                sensorData.lux = new ShellySensorLux();
+                            }
+                            sensorData.lux.isValid = true;
+                            sensorData.lux.value = (double) e.data.illuminance;
+                        }
+                        if (e.data.rotation != null) {
+                            if (sensorData.accel == null) {
+                                sensorData.accel = new ShellySensorAccel();
+                            }
+                            sensorData.accel.tilt = e.data.rotation.intValue();
+                        }
+
+                        if (e.data.buttonEvent != null) {
+                            ShellyInputState input = deviceStatus.inputs != null ? deviceStatus.inputs.get(0)
+                                    : new ShellyInputState();
+                            input.event = mapValue(MAP_INPUT_EVENT_TYPE, e.data.buttonEvent + "");
+                            input.eventCount++;
+                            deviceStatus.inputs.set(0, input);
+                            // sensorData.inputs.set(0, input);
+
+                            String group = getProfile().getInputGroup(0);
+                            String suffix = profile.getInputSuffix(0);
+                            t.updateChannel(group, CHANNEL_STATUS_EVENTTYPE + suffix, getStringType(input.event));
+                            t.updateChannel(group, CHANNEL_STATUS_EVENTCOUNT + suffix, getDecimal(input.eventCount));
+                            t.triggerButton(profile.getInputGroup(0), 0, input.event);
+                        }
+                        updated |= ShellyComponents.updateDeviceStatus(t, deviceStatus);
+                        updated |= ShellyComponents.updateSensors(getThing(), deviceStatus);
+                        break;
+                    default:
+                        super.onNotifyEvent(message);
+                }
+            }
+        } catch (ShellyApiException e) {
+            logger.debug("{}: Unable to process event", thingName, e);
+            t.incProtErrors();
+        }
+
+        if (updated) {
+
+        }
+    }
+
+    public static String buildBluServiceName(String name, String mac) throws IllegalArgumentException {
+        String model = name.contains("-") ? substringBefore(name, "-") : name; // e.g. SBBT-02C or just SBDW
+        switch (model) {
+            case SHELLYDT_BLUBUTTON:
+                return (THING_TYPE_SHELLYBLUBUTTON_STR + "-" + mac).toLowerCase();
+            case SHELLYDT_BLUDW:
+                return (THING_TYPE_SHELLYBLUDW_STR + "-" + mac).toLowerCase();
+            default:
+                throw new IllegalArgumentException("Unsupported BLU device model " + model);
+        }
+    }
+}
index 1e249b12ccc63eff7e9c659ffbaf98d7add0c55a..eb7ed5091e84adb0d59ba7bd87aeaa14e42ca2be 100755 (executable)
@@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 @NonNullByDefault
 public class ShellyThingConfiguration {
     public String deviceIp = ""; // ip address of thedevice
+    public String deviceAddress = ""; // IP address or MAC address for BLU devices
     public String userId = ""; // userid for http basic auth
     public String password = ""; // password for http basic auth
 
@@ -42,4 +43,6 @@ public class ShellyThingConfiguration {
     public String localIp = ""; // local ip addresses used to create callback url
     public String localPort = "8080";
     public String serviceName = "";
+
+    public boolean enableBluGateway = false;
 }
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyBluDiscoveryService.java
new file mode 100644 (file)
index 0000000..9b41422
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2023 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.discovery;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.core.thing.Thing.PROPERTY_MAC_ADDRESS;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.handler.ShellyThingTable;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Device discovery creates a thing in the inbox for each vehicle
+ * found in the data received from {@link ShellyBluDiscoveryService}.
+ *
+ * @author Markus Michels - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class ShellyBluDiscoveryService extends AbstractDiscoveryService {
+    private final Logger logger = LoggerFactory.getLogger(ShellyBluDiscoveryService.class);
+
+    private final BundleContext bundleContext;
+    private final ShellyThingTable thingTable;
+    private static final int TIMEOUT = 10;
+    private @Nullable ServiceRegistration<?> discoveryService;
+
+    public ShellyBluDiscoveryService(BundleContext bundleContext, ShellyThingTable thingTable) {
+        super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT);
+        this.bundleContext = bundleContext;
+        this.thingTable = thingTable;
+    }
+
+    @SuppressWarnings("null")
+    public void registerDeviceDiscoveryService() {
+        if (discoveryService == null) {
+            discoveryService = bundleContext.registerService(DiscoveryService.class.getName(), this,
+                    new Hashtable<String, Object>());
+        }
+    }
+
+    @Override
+    protected void startScan() {
+        logger.debug("Starting BLU Discovery");
+        thingTable.startScan();
+    }
+
+    public void discoveredResult(ThingTypeUID tuid, String model, String serviceName, String address,
+            Map<String, Object> properties) {
+        ThingUID uid = ShellyThingCreator.getThingUID(serviceName, model, "", true);
+        logger.debug("Adding discovered thing with id {}", uid.toString());
+        properties.put(PROPERTY_MAC_ADDRESS, address);
+        String thingLabel = "Shelly BLU " + model + " (" + serviceName + ")";
+        DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
+                .withRepresentationProperty(PROPERTY_DEV_NAME).withLabel(thingLabel).build();
+        thingDiscovered(result);
+    }
+
+    public void unregisterDeviceDiscoveryService() {
+        if (discoveryService != null) {
+            discoveryService.unregister();
+        }
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+        unregisterDeviceDiscoveryService();
+    }
+}
index 9d443d0237e217884d7059cbf126951a417765ee..664047b9014c999c7876bdb43086d8300ad022a6 100644 (file)
@@ -101,6 +101,10 @@ public class ShellyThingCreator {
     public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
     public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU";
 
+    // Shelly BLU Series
+    public static final String SHELLYDT_BLUBUTTON = "SBBT";
+    public static final String SHELLYDT_BLUDW = "SBDW";
+
     // Thing names
     public static final String THING_TYPE_SHELLY1_STR = "shelly1";
     public static final String THING_TYPE_SHELLY1L_STR = "shelly1l";
@@ -164,6 +168,12 @@ public class ShellyThingCreator {
     public static final String THING_TYPE_SHELLYPRO3EM_STR = "shellypro3em";
     public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm";
 
+    // Shelly BLU Series
+    public static final String THING_TYPE_SHELLYBLU_PREFIX = "shellyblu";
+    public static final String THING_TYPE_SHELLYBLUBUTTON_STR = THING_TYPE_SHELLYBLU_PREFIX + "button";
+    public static final String THING_TYPE_SHELLYBLUDW_STR = THING_TYPE_SHELLYBLU_PREFIX + "dw";
+
+    // Password protected or unknown device
     public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
     public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
 
@@ -258,6 +268,11 @@ public class ShellyThingCreator {
     public static final ThingTypeUID THING_TYPE_SHELLYPRO4PM = new ThingTypeUID(BINDING_ID,
             THING_TYPE_SHELLYPRO4PM_STR);
 
+    // Shelly Blu series
+    public static final ThingTypeUID THING_TYPE_SHELLYBLUBUTTON = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYBLUBUTTON_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYBLUDW = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBLUDW_STR);
+
     private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
     static {
         // mapping by device type id
@@ -324,6 +339,10 @@ public class ShellyThingCreator {
         THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM, THING_TYPE_SHELLYPRO4PM_STR);
         THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, THING_TYPE_SHELLYPRO4PM_STR);
 
+        // Blu Series
+        THING_TYPE_MAPPING.put(SHELLYDT_BLUBUTTON, THING_TYPE_SHELLYBLUBUTTON_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_BLUDW, THING_TYPE_SHELLYBLUDW_STR);
+
         // mapping by thing type
         THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1_STR, THING_TYPE_SHELLY1_STR);
         THING_TYPE_MAPPING.put(THING_TYPE_SHELLY1PM_STR, THING_TYPE_SHELLY1PM_STR);
index 332f4962de0353f2a213214840caeb61ce8a3ed8..dd808628826d79fd6c30e702244108753a7a9306 100755 (executable)
@@ -46,6 +46,7 @@ import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
 import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
 import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
 import org.openhab.binding.shelly.internal.api2.Shelly2ApiRpc;
+import org.openhab.binding.shelly.internal.api2.ShellyBluApi;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
@@ -104,6 +105,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
     private final int cacheCount = UPDATE_SETTINGS_INTERVAL_SECONDS / UPDATE_STATUS_INTERVAL_SECONDS;
 
     private final boolean gen2;
+    private final boolean blu;
     protected boolean autoCoIoT = false;
 
     // Thing status
@@ -151,7 +153,9 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
             gen = "2";
         }
         gen2 = "2".equals(gen);
-        this.api = !gen2 ? new Shelly1HttpApi(thingName, this) : new Shelly2ApiRpc(thingName, thingTable, this);
+        blu = thingType.startsWith("shellyblu");
+        this.api = !blu ? !gen2 ? new Shelly1HttpApi(thingName, this) : new Shelly2ApiRpc(thingName, thingTable, this)
+                : new ShellyBluApi(thingName, thingTable, this);
         if (gen2) {
             config.eventsCoIoT = false;
         }
@@ -162,7 +166,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
 
     @Override
     public boolean checkRepresentation(String key) {
-        return key.equalsIgnoreCase(getUID()) || key.equalsIgnoreCase(config.deviceIp)
+        return key.equalsIgnoreCase(getUID()) || key.equalsIgnoreCase(config.deviceAddress)
                 || key.equalsIgnoreCase(config.serviceName) || key.equalsIgnoreCase(getThingName());
     }
 
@@ -176,15 +180,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
             boolean start = true;
             try {
                 initializeThingConfig();
-                logger.debug("{}: Device config: IP address={}, HTTP user/password={}/{}, update interval={}",
-                        thingName, config.deviceIp, config.userId.isEmpty() ? "<non>" : config.userId,
+                logger.debug("{}: Device config: Device address={}, HTTP user/password={}/{}, update interval={}",
+                        thingName, config.deviceAddress, config.userId.isEmpty() ? "<non>" : config.userId,
                         config.password.isEmpty() ? "<none>" : "***", config.updateInterval);
                 logger.debug(
                         "{}: Configured Events: Button: {}, Switch (on/off): {}, Push: {}, Roller: {}, Sensor: {}, CoIoT: {}, Enable AutoCoIoT: {}",
                         thingName, config.eventsButton, config.eventsSwitch, config.eventsPush, config.eventsRoller,
                         config.eventsSensorReport, config.eventsCoIoT, bindingConfig.autoCoIoT);
-                updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
-                        messages.get("status.unknown.initializing"));
                 start = initializeThing();
             } catch (ShellyApiException e) {
                 ShellyApiResult res = e.getApiResult();
@@ -225,6 +227,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         return httpClient;
     }
 
+    @Override
+    public void startScan() {
+        if (api.isInitialized()) {
+            api.startScan();
+        }
+    }
+
     /**
      * This routine is called every time the Thing configuration has been changed
      */
@@ -257,12 +266,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         resetStats();
 
         logger.debug("{}: Start initializing for thing {}, type {}, IP address {}, Gen2: {}, CoIoT: {}", thingName,
-                getThing().getLabel(), thingType, config.deviceIp, gen2, config.eventsCoIoT);
-        if (config.deviceIp.isEmpty()) {
-            setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-ip");
+                getThing().getLabel(), thingType, config.deviceAddress, gen2, config.eventsCoIoT);
+        if (config.deviceAddress.isEmpty()) {
+            setThingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "config-status.error.missing-device-address");
             return false;
         }
 
+        updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING,
+                messages.get("status.unknown.initializing"));
+
         profile.initFromThingType(thingType); // do some basic initialization
 
         // Gen 1 only: Setup CoAP listener to we get the CoAP message, which triggers initialization even the thing
@@ -326,7 +338,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         checkVersion(tmpPrf, tmpPrf.status);
 
         startCoap(config, tmpPrf);
-        if (!gen2) {
+        if (!gen2 && !blu) {
             api.setActionURLs(); // register event urls
         }
 
@@ -358,7 +370,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
                 return;
             }
 
-            if (!profile.isInitialized() || (isThingOffline() && profile.alwaysOn)) {
+            if (!profile.isInitialized()) {
                 logger.debug("{}: {}", thingName, messages.get("command.init", command));
                 initializeThing();
             } else {
@@ -468,6 +480,15 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
                 logger.warn("{}: {} - {}", thingName, messages.get("command.failed", command, channelUID),
                         e.toString());
             }
+
+            String group = getString(channelUID.getGroupId());
+            String channel = getString(channelUID.getIdWithoutGroup());
+            State oldValue = getChannelValue(group, channel);
+            if (oldValue != UnDefType.NULL) {
+                logger.info("{}: Restore channel value to {}", thingName, oldValue);
+                updateChannel(group, channel, oldValue);
+            }
+
         } catch (IllegalArgumentException e) {
             logger.debug("{}: {}", thingName, messages.get("command.failed", command, channelUID));
         }
@@ -518,8 +539,11 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
                     postEvent(ALARM_TYPE_RESTARTED, true);
                 }
 
-                // If status update was successful the thing must be online
-                setThingOnline();
+                // If status update was successful the thing must be online,
+                // but not while firmware update is in progress
+                if (getThingStatusDetail() != ThingStatusDetail.FIRMWARE_UPDATING) {
+                    setThingOnline();
+                }
 
                 // map status to channels
                 updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_NAME, getStringType(profile.settings.name));
@@ -587,13 +611,13 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         logger.debug("{}: Shelly settings info for {}: {}", thingName, profile.hostname, profile.settingsJson);
         logger.debug("{}: Device "
                 + "hasRelays:{} (numRelays={}),isRoller:{} (numRoller={}),isDimmer:{},numMeter={},isEMeter:{}), ext. Switch Add-On: {}"
-                + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}"
+                + ",isSensor:{},isDS:{},hasBattery:{}{},isSense:{},isMotion:{},isLight:{},isBulb:{},isDuo:{},isRGBW2:{},inColor:{}, BLU Gateway support: {}"
                 + ",alwaysOn:{}, updatePeriod:{}sec", thingName, profile.hasRelays, profile.numRelays, profile.isRoller,
                 profile.numRollers, profile.isDimmer, profile.numMeters, profile.isEMeter,
                 profile.settings.extSwitch != null ? "installed" : "n/a", profile.isSensor, profile.isDW,
                 profile.hasBattery, profile.hasBattery ? " (low battery threshold=" + config.lowBattery + "%)" : "",
                 profile.isSense, profile.isMotion, profile.isLight, profile.isBulb, profile.isDuo, profile.isRGBW2,
-                profile.inColor, profile.alwaysOn, profile.updatePeriod);
+                profile.inColor, profile.alwaysOn, profile.updatePeriod, config.enableBluGateway);
         if (profile.status.extTemperature != null || profile.status.extHumidity != null
                 || profile.status.extVoltage != null || profile.status.extAnalogInput != null) {
             logger.debug("{}: Shelly Add-On detected with at least 1 external sensor", thingName);
@@ -715,7 +739,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
 
         // Update uptime and WiFi, internal temp
         ShellyComponents.updateDeviceStatus(this, status);
-        stats.wifiRssi = status.wifiSta.rssi;
+        stats.wifiRssi = getInteger(status.wifiSta.rssi);
 
         if (api.isInitialized()) {
             stats.timeoutErrors = api.getTimeoutErrors();
@@ -829,9 +853,10 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
      * @return true if event was processed
      */
     @Override
-    public boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String type,
+    public boolean onEvent(String address, String deviceName, String deviceIndex, String type,
             Map<String, String> parameters) {
-        if (thingName.equalsIgnoreCase(deviceName) || config.deviceIp.equals(ipAddress)) {
+        if (thingName.equalsIgnoreCase(deviceName) || config.deviceAddress.equals(address)
+                || config.serviceName.equals(deviceName)) {
             logger.debug("{}: Event received: class={}, index={}, parameters={}", deviceName, type, deviceIndex,
                     parameters);
             int idx = !deviceIndex.isEmpty() ? Integer.parseInt(deviceIndex) : 1;
@@ -851,7 +876,7 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
                 String payload = "";
                 String parmType = getString(parameters.get("type"));
                 String event = !parmType.isEmpty() ? parmType : type;
-                boolean isButton = profile.inButtonMode(idx - 1);
+                boolean isButton = profile.inButtonMode(idx - 1) || type.equals("button");
                 switch (event) {
                     case SHELLY_EVENT_SHORTPUSH:
                     case SHELLY_EVENT_DOUBLE_SHORTPUSH:
@@ -959,23 +984,31 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
         thingName = getString(properties.get(PROPERTY_SERVICE_NAME));
         if (thingName.isEmpty()) {
             thingName = getString(thingType + "-" + getString(getThing().getUID().getId())).toLowerCase();
-            logger.debug("{}: Thing name derived from UID {}", thingName, getString(getThing().getUID().toString()));
         }
 
         config = getConfigAs(ShellyThingConfiguration.class);
-        if (config.deviceIp.isEmpty()) {
-            logger.debug("{}: IP address for the device must not be empty", thingName); // may not set in .things file
+        if (config.deviceAddress.isEmpty()) {
+            config.deviceAddress = config.deviceIp;
+        }
+        if (config.deviceAddress.isEmpty()) {
+            logger.debug("{}: IP/MAC address for the device must not be empty", thingName); // may not set in .things
+                                                                                            // file
             return;
         }
-        try {
-            InetAddress addr = InetAddress.getByName(config.deviceIp);
-            String saddr = addr.getHostAddress();
-            if (!config.deviceIp.equals(saddr)) {
-                logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
-                config.deviceIp = saddr;
+
+        config.deviceAddress = config.deviceAddress.toLowerCase().replaceAll(":", ""); // remove : from MAC address and
+                                                                                       // convert to lower case
+        if (!config.deviceIp.isEmpty()) {
+            try {
+                InetAddress addr = InetAddress.getByName(config.deviceIp);
+                String saddr = addr.getHostAddress();
+                if (!config.deviceIp.equals(saddr)) {
+                    logger.debug("{}: hostname {} resolved to IP address {}", thingName, config.deviceIp, saddr);
+                    config.deviceIp = saddr;
+                }
+            } catch (UnknownHostException e) {
+                logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
             }
-        } catch (UnknownHostException e) {
-            logger.debug("{}: Unable to resolve hostname {}", thingName, config.deviceIp);
         }
 
         config.serviceName = getString(properties.get(PROPERTY_SERVICE_NAME));
@@ -1406,9 +1439,11 @@ public abstract class ShellyBaseHandler extends BaseThingHandler
             properties.put(PROPERTY_MAC_ADDRESS, profile.mac);
             properties.put(PROPERTY_FIRMWARE_VERSION, profile.fwVersion + "/" + profile.fwDate);
             properties.put(PROPERTY_DEV_MODE, profile.mode);
-            properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
-            properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
-            properties.put(PROPERTY_NUM_METER, String.valueOf(profile.numMeters));
+            if (profile.hasRelays) {
+                properties.put(PROPERTY_NUM_RELAYS, String.valueOf(profile.numRelays));
+                properties.put(PROPERTY_NUM_ROLLERS, String.valueOf(profile.numRollers));
+                properties.put(PROPERTY_NUM_METER, String.valueOf(profile.numMeters));
+            }
             properties.put(PROPERTY_UPDATE_PERIOD, String.valueOf(profile.updatePeriod));
             if (!profile.hwRev.isEmpty()) {
                 properties.put(PROPERTY_HWREV, profile.hwRev);
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBluSensorHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBluSensorHandler.java
new file mode 100644 (file)
index 0000000..3cd8975
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2023 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 static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api2.ShellyBluApi.buildBluServiceName;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+import static org.openhab.core.thing.Thing.PROPERTY_MODEL_ID;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
+import org.openhab.binding.shelly.internal.api2.Shelly2ApiJsonDTO.Shelly2NotifyEvent;
+import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
+import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link ShellyBluSensorHandler} implements the thing handler for the BLU devices
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class ShellyBluSensorHandler extends ShellyBaseHandler {
+    private final static Logger logger = LoggerFactory.getLogger(ShellyBluSensorHandler.class);
+
+    public ShellyBluSensorHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
+            final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
+            final Shelly1CoapServer coapServer, final HttpClient httpClient) {
+        super(thing, translationProvider, bindingConfig, thingTable, coapServer, httpClient);
+    }
+
+    @Override
+    public void initialize() {
+        logger.debug("Thing is using  {}", this.getClass());
+        super.initialize();
+    }
+
+    public static void addBluThing(String gateway, Shelly2NotifyEvent e, ShellyThingTable thingTable) {
+        String model = substringBefore(getString(e.data.name), "-").toUpperCase();
+        String mac = e.data.addr.replaceAll(":", "");
+        String ttype = "";
+        logger.debug("{}: Create thing for new BLU device {}: {} / {}", gateway, e.data.name, model, mac);
+        ThingTypeUID tuid;
+        switch (model) {
+            case SHELLYDT_BLUBUTTON:
+                ttype = THING_TYPE_SHELLYBLUBUTTON_STR;
+                tuid = THING_TYPE_SHELLYBLUBUTTON;
+                break;
+            case SHELLYDT_BLUDW:
+                ttype = THING_TYPE_SHELLYBLUDW_STR;
+                tuid = THING_TYPE_SHELLYBLUDW;
+                break;
+            default:
+                logger.debug("{}: Unsupported BLU device model {}, MAC={}", gateway, model, mac);
+                return;
+        }
+        String serviceName = buildBluServiceName(model, mac);
+
+        Map<String, Object> properties = new TreeMap<>();
+        addProperty(properties, PROPERTY_MODEL_ID, model);
+        addProperty(properties, PROPERTY_SERVICE_NAME, serviceName);
+        addProperty(properties, PROPERTY_DEV_NAME, e.data.name);
+        addProperty(properties, PROPERTY_DEV_TYPE, ttype);
+        addProperty(properties, PROPERTY_DEV_GEN, "BLU");
+        addProperty(properties, PROPERTY_GW_DEVICE, gateway);
+        addProperty(properties, CONFIG_DEVICEADDRESS, mac);
+
+        if (thingTable != null) {
+            thingTable.discoveredResult(tuid, model, serviceName, mac, properties);
+        }
+    }
+
+    private static void addProperty(Map<String, Object> properties, String key, @Nullable String value) {
+        properties.put(key, value != null ? value : "");
+    }
+}
index 7846afde876a33c60103e10c5e1a92b083ef4a1f..3e6e0a22919d16ba63b613cfdefe5168c0cdb894 100644 (file)
@@ -54,6 +54,9 @@ public class ShellyComponents {
     public static boolean updateDeviceStatus(ShellyThingInterface thingHandler, ShellySettingsStatus status) {
         ShellyDeviceProfile profile = thingHandler.getProfile();
 
+        if (!profile.gateway.isEmpty()) {
+            thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_GATEWAY, getStringType(profile.gateway));
+        }
         if (!thingHandler.areChannelsCreated()) {
             thingHandler.updateChannelDefinitions(ShellyChannelDefinitions.createDeviceChannels(thingHandler.getThing(),
                     thingHandler.getProfile(), status));
@@ -177,6 +180,8 @@ public class ShellyComponents {
                     break;
             }
             if (pos != -1) {
+                thingHandler.logger.debug("{}: Update roller position to {}/{}, state={}", thingHandler.thingName, pos,
+                        SHELLY_MAX_ROLLER_POS - pos, state);
                 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_CONTROL,
                         toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
                 updated |= thingHandler.updateChannel(groupName, CHANNEL_ROL_CONTROL_POS,
@@ -238,7 +243,7 @@ public class ShellyComponents {
 
                             // convert Watt/Min to kw/h
                             if (meter.total != null) {
-                                double kwh = getDouble(meter.total) / 60 / 1000;
+                                double kwh = getDouble(meter.total) / 1000 / 60;
                                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
                                         toQuantityType(kwh, DIGITS_KWH, Units.KILOWATT_HOUR));
                                 accumulatedTotal += kwh;
@@ -263,13 +268,15 @@ public class ShellyComponents {
                                         .createEMeterChannels(thingHandler.getThing(), profile, emeter, groupName));
                             }
 
-                            // convert Watt/Hour tok w/h
+                            // convert Watt/Hour to kw/h
+                            double total = getDouble(emeter.total) / 1000 / 60;
+                            double totalReturned = getDouble(emeter.totalReturned) / 1000;
                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
                                     toQuantityType(getDouble(emeter.power), DIGITS_WATT, Units.WATT));
                             updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
-                                    toQuantityType(getDouble(emeter.total) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
-                            updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET, toQuantityType(
-                                    getDouble(emeter.totalReturned) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
+                                    toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
+                            updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_TOTALRET,
+                                    toQuantityType(totalReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_REACTWATTS,
                                     toQuantityType(getDouble(emeter.reactive), DIGITS_WATT, Units.WATT));
                             updated |= thingHandler.updateChannel(groupName, CHANNEL_EMETER_VOLTAGE,
@@ -280,8 +287,8 @@ public class ShellyComponents {
                                     toQuantityType(computePF(emeter), Units.PERCENT));
 
                             accumulatedWatts += getDouble(emeter.power);
-                            accumulatedTotal += getDouble(emeter.total) / 1000;
-                            accumulatedReturned += getDouble(emeter.totalReturned) / 1000;
+                            accumulatedTotal += total;
+                            accumulatedReturned += totalReturned;
                             if (updated) {
                                 thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE, getTimestamp());
                             }
@@ -327,7 +334,7 @@ public class ShellyComponents {
                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_CURRENTWATTS,
                         toQuantityType(getDouble(currentWatts), DIGITS_WATT, Units.WATT));
                 updated |= thingHandler.updateChannel(groupName, CHANNEL_METER_TOTALKWH,
-                        toQuantityType(getDouble(totalWatts), DIGITS_KWH, Units.KILOWATT_HOUR));
+                        toQuantityType(totalWatts, DIGITS_KWH, Units.KILOWATT_HOUR));
 
                 if (updated && timestamp > 0) {
                     thingHandler.updateChannel(groupName, CHANNEL_LAST_UPDATE,
@@ -336,12 +343,14 @@ public class ShellyComponents {
             }
 
             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_ACCUWATTS, toQuantityType(
+                        status.totalPower != null ? status.totalPower : accumulatedWatts, DIGITS_WATT, Units.WATT));
                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCUTOTAL,
-                        toQuantityType(accumulatedTotal, DIGITS_KWH, Units.KILOWATT_HOUR));
+                        toQuantityType(status.totalCurrent != null ? status.totalCurrent / 1000 : accumulatedTotal,
+                                DIGITS_KWH, Units.KILOWATT_HOUR));
                 thingHandler.updateChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ACCURETURNED,
-                        toQuantityType(accumulatedReturned, DIGITS_KWH, Units.KILOWATT_HOUR));
+                        toQuantityType(status.totalReturned != null ? status.totalReturned / 1000 : accumulatedReturned,
+                                DIGITS_KWH, Units.KILOWATT_HOUR));
             }
         }
 
index 98cbf7ea6d1f526228a43147a0d3409524fb3518..187c1875924c4a6c0bec76e91c49a6c431aeba87 100644 (file)
@@ -28,6 +28,6 @@ public interface ShellyDeviceListener {
     /**
      * This method is called when new device information is received.
      */
-    boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String eventType,
+    public boolean onEvent(String ipAddress, String deviceName, String deviceIndex, String eventType,
             Map<String, String> parameters);
 }
index 1f64e2b2b56dbfcf12745f4f51719cb3fbcdda83..94ba3d8291ad3362f7d34164dda3dbae4eee6c43 100644 (file)
@@ -56,15 +56,6 @@ public class ShellyLightHandler extends ShellyBaseHandler {
     private final Logger logger = LoggerFactory.getLogger(ShellyLightHandler.class);
     private final Map<Integer, ShellyColorUtils> channelColors;
 
-    /**
-     * Constructor
-     *
-     * @param thing The thing passed by the HandlerFactory
-     * @param bindingConfig configuration of the binding
-     * @param coapServer coap server instance
-     * @param localIP local IP of the openHAB host
-     * @param httpPort port of the openHAB HTTP API
-     */
     public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
             final ShellyBindingConfiguration bindingConfig, final ShellyThingTable thingTable,
             final Shelly1CoapServer coapServer, final HttpClient httpClient) {
@@ -230,6 +221,7 @@ public class ShellyLightHandler extends ShellyBaseHandler {
         }
     }
 
+    @SuppressWarnings("deprecation")
     private boolean handleColorPicker(ShellyDeviceProfile profile, Integer lightId, ShellyColorUtils col,
             Command command) throws ShellyApiException {
         boolean updated = false;
index a4600ef0952f4ded96d3301729fffbf1fab0ab67..fbe28c453d547e0a1178c8e0786d15208d677cd8 100644 (file)
@@ -28,29 +28,29 @@ import org.openhab.core.types.State;
 @NonNullByDefault
 public interface ShellyManagerInterface {
 
-    Thing getThing();
+    public Thing getThing();
 
-    String getThingName();
+    public String getThingName();
 
-    ShellyDeviceProfile getProfile();
+    public ShellyDeviceProfile getProfile();
 
-    ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
+    public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
 
-    ShellyApiInterface getApi();
+    public ShellyApiInterface getApi();
 
-    ShellyDeviceStats getStats();
+    public ShellyDeviceStats getStats();
 
-    void resetStats();
+    public void resetStats();
 
-    State getChannelValue(String group, String channel);
+    public State getChannelValue(String group, String channel);
 
-    void setThingOnline();
+    public void setThingOnline();
 
-    void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
+    public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
 
-    boolean requestUpdates(int requestCount, boolean refreshSettings);
+    public boolean requestUpdates(int requestCount, boolean refreshSettings);
 
-    void incProtMessages();
+    public void incProtMessages();
 
-    void incProtErrors();
+    public void incProtErrors();
 }
index 8fc932094d279fc32f6db689d38aed7c06fd0e68..2285f66f2cb37812818902a16b30f3fa5226b8f2 100644 (file)
@@ -39,84 +39,85 @@ import org.openhab.core.types.StateOption;
 @NonNullByDefault
 public interface ShellyThingInterface {
 
-    ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
+    public ShellyDeviceProfile getProfile(boolean forceRefresh) throws ShellyApiException;
 
-    @Nullable
-    List<StateOption> getStateOptions(ChannelTypeUID uid);
+    public @Nullable List<StateOption> getStateOptions(ChannelTypeUID uid);
 
-    double getChannelDouble(String group, String channel);
+    public double getChannelDouble(String group, String channel);
 
-    boolean updateChannel(String group, String channel, State value);
+    public boolean updateChannel(String group, String channel, State value);
 
-    boolean updateChannel(String channelId, State value, boolean force);
+    public boolean updateChannel(String channelId, State value, boolean force);
 
-    void setThingOnline();
+    public void setThingOnline();
 
-    void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
+    public void setThingOffline(ThingStatusDetail detail, String messageKey, Object... arguments);
 
-    boolean isStopping();
+    public boolean isStopping();
 
-    String getThingType();
+    public String getThingType();
 
-    ThingStatus getThingStatus();
+    public ThingStatus getThingStatus();
 
-    ThingStatusDetail getThingStatusDetail();
+    public ThingStatusDetail getThingStatusDetail();
 
-    boolean isThingOnline();
+    public boolean isThingOnline();
 
-    boolean requestUpdates(int requestCount, boolean refreshSettings);
+    public boolean requestUpdates(int requestCount, boolean refreshSettings);
 
-    void triggerUpdateFromCoap();
+    public void triggerUpdateFromCoap();
 
-    void reinitializeThing();
+    public void reinitializeThing();
 
-    void restartWatchdog();
+    public void restartWatchdog();
 
-    void publishState(String channelId, State value);
+    public void publishState(String channelId, State value);
 
-    boolean areChannelsCreated();
+    public boolean areChannelsCreated();
 
-    State getChannelValue(String group, String channel);
+    public State getChannelValue(String group, String channel);
 
-    boolean updateInputs(ShellySettingsStatus status);
+    public boolean updateInputs(ShellySettingsStatus status);
 
-    void updateChannelDefinitions(Map<String, Channel> dynChannels);
+    public void updateChannelDefinitions(Map<String, Channel> dynChannels);
 
-    void postEvent(String event, boolean force);
+    public void postEvent(String event, boolean force);
 
-    void triggerChannel(String group, String channelID, String event);
+    public void triggerChannel(String group, String channelID, String event);
 
-    void triggerButton(String group, int idx, String value);
+    public void triggerButton(String group, int idx, String value);
 
-    ShellyDeviceStats getStats();
+    public ShellyDeviceStats getStats();
 
-    void resetStats();
+    public void resetStats();
 
-    Thing getThing();
+    public Thing getThing();
 
-    String getThingName();
+    public String getThingName();
 
-    ShellyThingConfiguration getThingConfig();
+    public ShellyThingConfiguration getThingConfig();
 
-    HttpClient getHttpClient();
+    public HttpClient getHttpClient();
 
-    String getProperty(String key);
+    public String getProperty(String key);
 
-    void updateProperties(String key, String value);
+    public void updateProperties(String key, String value);
 
-    boolean updateWakeupReason(@Nullable List<Object> valueArray);
+    public boolean updateWakeupReason(@Nullable List<Object> valueArray);
 
-    ShellyApiInterface getApi();
+    public ShellyApiInterface getApi();
 
-    ShellyDeviceProfile getProfile();
+    public ShellyDeviceProfile getProfile();
 
-    long getScheduledUpdates();
+    public long getScheduledUpdates();
 
-    void fillDeviceStatus(ShellySettingsStatus status, boolean updated);
+    public void fillDeviceStatus(ShellySettingsStatus status, boolean updated);
 
-    boolean checkRepresentation(String key);
+    public boolean checkRepresentation(String key);
 
-    void incProtMessages();
+    public void incProtMessages();
 
-    void incProtErrors();
+    public void incProtErrors();
+
+    public void startScan();
 }
index 2719b80403cf965cf557e905a050828d40a96b5c..f21318672a552f8242979b458d12871affed526b 100644 (file)
@@ -16,8 +16,13 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.discovery.ShellyBluDiscoveryService;
+import org.openhab.core.thing.ThingTypeUID;
+import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
 
 /***
  * The{@link ShellyThingTable} implements a simple table to allow dispatching incoming events to the proper thing
@@ -29,6 +34,7 @@ import org.osgi.service.component.annotations.ConfigurationPolicy;
 @Component(service = ShellyThingTable.class, configurationPolicy = ConfigurationPolicy.OPTIONAL)
 public class ShellyThingTable {
     private Map<String, ShellyThingInterface> thingTable = new ConcurrentHashMap<>();
+    private @Nullable ShellyBluDiscoveryService bluDiscoveryService;
 
     public void addThing(String key, ShellyThingInterface thing) {
         if (thingTable.containsKey(key)) {
@@ -37,7 +43,7 @@ public class ShellyThingTable {
         thingTable.put(key, thing);
     }
 
-    public ShellyThingInterface getThing(String key) {
+    public @Nullable ShellyThingInterface findThing(String key) {
         ShellyThingInterface t = thingTable.get(key);
         if (t != null) {
             return t;
@@ -48,7 +54,15 @@ public class ShellyThingTable {
                 return t;
             }
         }
-        throw new IllegalArgumentException();
+        return null;
+    }
+
+    public ShellyThingInterface getThing(String key) {
+        ShellyThingInterface t = findThing(key);
+        if (t == null) {
+            throw new IllegalArgumentException();
+        }
+        return t;
     }
 
     public void removeThing(String key) {
@@ -64,4 +78,36 @@ public class ShellyThingTable {
     public int size() {
         return thingTable.size();
     }
+
+    public void startDiscoveryService(BundleContext bundleContext) {
+        if (bluDiscoveryService == null) {
+            bluDiscoveryService = new ShellyBluDiscoveryService(bundleContext, this);
+            bluDiscoveryService.registerDeviceDiscoveryService();
+        }
+    }
+
+    public void startScan() {
+        for (Map.Entry<String, ShellyThingInterface> thing : thingTable.entrySet()) {
+            (thing.getValue()).startScan();
+        }
+    }
+
+    public void stopDiscoveryService() {
+        if (bluDiscoveryService != null) {
+            bluDiscoveryService.unregisterDeviceDiscoveryService();
+            bluDiscoveryService = null;
+        }
+    }
+
+    public void discoveredResult(ThingTypeUID uid, String model, String serviceName, String address,
+            Map<String, Object> properties) {
+        if (bluDiscoveryService != null) {
+            bluDiscoveryService.discoveredResult(uid, model, serviceName, address, properties);
+        }
+    }
+
+    @Deactivate
+    public void deactivate() {
+        stopDiscoveryService();
+    }
 }
index 68b2f9106edad2adb84e15be107cbf3b686e2c5f..8fffe8cede3e65d52c84396a072405a87b0365f7 100644 (file)
@@ -87,6 +87,7 @@ public class ShellyManagerCache<K, V> extends ConcurrentHashMap<K, V> {
             }
         }
 
+        @SuppressWarnings("null")
         private void cleanMap() {
             long currentTime = new Date().getTime();
             for (K key : timeMap.keySet()) {
index ac78b066c39ad5b92a3aa79c452d955c10261d80..4bc16c72db1494c6c6b5c877e0d46aa1390facc3 100644 (file)
@@ -61,7 +61,6 @@ public class ShellyManagerConstants {
     public static final String ACTION_GETDEB1 = "getdebug1";
     public static final String ACTION_NONE = "-";
 
-    public static final String TEMPLATE_PATH = "sniplets/";
     public static final String HEADER_HTML = "header.html";
     public static final String OVERVIEW_HTML = "overview.html";
     public static final String OVERVIEW_HEADER = "ov_header.html";
index bce0477f0eaa2749192ebd628cf0805b16325ee1..7de4246c40fc169de62879f6f89508ac9a96aca0 100644 (file)
@@ -164,7 +164,7 @@ public class ShellyManagerPage {
         }
 
         String html = "";
-        String file = TEMPLATE_PATH + template;
+        String file = BUNDLE_RESOURCE_SNIPLETS + "/" + template;
         logger.debug("Read HTML from {}", file);
         ClassLoader cl = ShellyManagerInterface.class.getClassLoader();
         if (cl != null) {
index 41d4e1344815d0368df1d0a1f4acec5191f93dce..e3a5fedcf526e08044ae258ed999874f940f2bc0 100644 (file)
@@ -84,6 +84,7 @@ public class ShellyManagerServlet extends HttpServlet {
         logger.debug("{} stopped", className);
     }
 
+    @SuppressWarnings("resource")
     @Override
     protected void service(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response)
             throws ServletException, IOException, IllegalArgumentException {
index 7dd2d9c3d37d7c6139948855d5ffb4ba9faacaeb..71dfe33d4cf6dee65f452c144544b9eeb84ba4d1 100644 (file)
@@ -125,6 +125,7 @@ public class ShellyChannelDefinitions {
         CHANNEL_DEFINITIONS
                 // Device
                 .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_NAME, "deviceName", ITEMT_STRING))
+                .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_GATEWAY, "gatewayDevice", ITEMT_STRING))
                 .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ITEMP, "deviceTemp", ITEMT_TEMP))
                 .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_WAKEUP, "sensorWakeup", ITEMT_STRING))
                 .add(new ShellyChannel(m, CHGR_DEVST, CHANNEL_DEVST_ACCUWATTS, "meterAccuWatts", ITEMT_POWER))
@@ -297,9 +298,11 @@ public class ShellyChannelDefinitions {
         Map<String, Channel> add = new LinkedHashMap<>();
 
         addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
+        addChannel(thing, add, !profile.gateway.isEmpty() || profile.isBlu, CHGR_DEVST, CHANNEL_DEVST_GATEWAY);
 
-        if (!profile.isSensor && !profile.isIX && status.temperature != null
-                && status.temperature != SHELLY_API_INVTEMP) {
+        if (!profile.isSensor && !profile.isIX
+                && ((status.temperature != null && getDouble(status.temperature) != SHELLY_API_INVTEMP)
+                        || (status.tmp != null && getDouble(status.tmp.tC) != SHELLY_API_INVTEMP))) {
             // Only some devices report the internal device temp
             addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP);
         }
@@ -355,6 +358,8 @@ public class ShellyChannelDefinitions {
             addChannel(thing, add, profile.status.extTemperature.sensor1 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP1);
             addChannel(thing, add, profile.status.extTemperature.sensor2 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP2);
             addChannel(thing, add, profile.status.extTemperature.sensor3 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP3);
+            addChannel(thing, add, profile.status.extTemperature.sensor4 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP4);
+            addChannel(thing, add, profile.status.extTemperature.sensor5 != null, CHGR_SENSOR, CHANNEL_ESENSOR_TEMP5);
         }
         addChannel(thing, add, profile.status.extHumidity != null, CHGR_SENSOR, CHANNEL_ESENSOR_HUMIDITY);
         addChannel(thing, add, profile.status.extVoltage != null, CHGR_SENSOR, CHANNEL_ESENSOR_VOLTAGE);
@@ -411,7 +416,7 @@ public class ShellyChannelDefinitions {
             for (int i = 0; i < profile.numInputs; i++) {
                 String group = profile.getInputGroup(i);
                 String suffix = profile.getInputSuffix(i); // multi ? String.valueOf(i + 1) : "";
-                addChannel(thing, add, true, group, CHANNEL_INPUT + suffix);
+                addChannel(thing, add, !profile.isButton, group, CHANNEL_INPUT + suffix);
                 addChannel(thing, add, true, group,
                         (!profile.isRoller ? CHANNEL_BUTTON_TRIGGER + suffix : CHANNEL_EVENT_TRIGGER));
                 if (profile.inButtonMode(i)) {
index 895e7dbca98f29907cc29ccc46f2c681561b2acd..92076c4f46e0190b799519b745675b0267db6ce0 100644 (file)
@@ -6,7 +6,6 @@
        <type>binding</type>
        <name>@text/addon.shelly.name</name>
        <description>@text/addon.shelly.description</description>
-       <connection>local</connection>
 
        <config-description>
                <parameter name="defaultUserId" type="text">
index 9d368767c892a2b21d9d6db333428446b351b287..bdc569513e71b2d493a088b3e45b748456128af7 100644 (file)
                        <unitLabel>seconds</unitLabel>
                        <advanced>true</advanced>
                </parameter>
+               <parameter name="enableBluGateway" type="boolean" required="false">
+                       <label>@text/thing-type.config.shelly.enableBluGateway.label</label>
+                       <description>@text/thing-type.config.shelly.enableBluGateway.description</description>
+                       <default>false</default>
+               </parameter>
        </config-description>
 
        <config-description uri="thing-type:shelly:roller-gen2">
                        <unitLabel>seconds</unitLabel>
                        <advanced>true</advanced>
                </parameter>
+               <parameter name="enableBluGateway" type="boolean" required="false">
+                       <label>@text/thing-type.config.shelly.enableBluGateway.label</label>
+                       <description>@text/thing-type.config.shelly.enableBluGateway.description</description>
+                       <default>false</default>
+               </parameter>
        </config-description>
 
        <config-description uri="thing-type:shelly:battery-gen2">
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/configblu.xml b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/config/configblu.xml
new file mode 100644 (file)
index 0000000..fa888ad
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:shelly:blubattery">
+               <parameter name="deviceAddress" type="text" required="true">
+                       <label>@text/thing-type.config.shelly.deviceAddress.label</label>
+                       <description>@text/thing-type.config.shelly.@text/thing-type.config.shelly.deviceAddress.label.description</description>
+               </parameter>
+               <parameter name="lowBattery" type="integer" required="false">
+                       <label>@text/thing-type.config.shelly.battery.lowBattery.label</label>
+                       <description>@text/thing-type.config.shelly.battery.lowBattery.description</description>
+                       <default>20</default>
+                       <unitLabel>%</unitLabel>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
index bec2fb5ce18db0f8b0cb4ad1a7ed6414a6e3fa68..94d969f5a7e452041a985ba3cdfc24831f286460 100644 (file)
@@ -11,7 +11,7 @@ addon.shelly.config.autoCoIoT.label = Auto-CoIoT
 addon.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.  
 
 # Config status messages
-message.config-status.error.missing-device-ip = IP address of the Shelly device is missing.
+message.config-status.error.missing-device-address = IP/MAC Address of the Shelly device is missing.
 message.config-status.error.missing-userid = No user ID in the Thing configuration
 
 # Thing status descriptions
@@ -43,7 +43,7 @@ message.event.filtered = Event filtered: {0}
 message.coap.init.failed = Unable to start CoIoT: {0}
 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.discovery.failed = Device discovery of device  with address {0} failed: {1}
 message.roller.calibrating = Device is not calibrated, use Shelly App to perform initial roller calibration.
 message.roller.favmissing = Roller position favorites are not supported by installed firmware or not configured in the Shelly App
 
@@ -88,16 +88,6 @@ thing-type.shelly.shellytrv.description = Shelly TRV (Radiator value, battery po
 thing-type.shelly.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
 thing-type.shelly.shellypludht.description = Shelly Plus HT - Temperature and Humidity Sensor
 
-# Plus Devices
-thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
-thing-type.shelly.shellyplus1pm.description =  Shelly Plus 1PM - Single Relay Switch with Power Meter
-thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
-thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
-thing-type.shelly.shellyplusplug.description = Shelly Plus Plug S/IT/UK/US . Outlet with Power Meter
-thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
-thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
-thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
-
 # Pro Devices
 thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
 thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
@@ -108,16 +98,18 @@ thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
 thing-type.shelly.shellypro3em.description = Shelly Pro 3EM - 3xPower Meter
 thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
 
-
-# Plus/Pro devices
+# Plus devices
  thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
  thing-type.shelly.shellyplus1pm.description =  Shelly Plus 1PM - Single Relay Switch with Power Meter
  thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
  thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
+ thing-type.shelly.shellyplusplug.description = Shelly Plus Plug S/IT/UK/US . Outlet with Power Meter
  thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
  thing-type.shelly.shellyplussmoke.description = Shelly Plus Smoke - Smoke Detector with Alarm
  thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
  thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
+ # Pro devices
  thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
  thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
  thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch
@@ -127,15 +119,23 @@ thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch wit
  thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
  thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
  
+ # BLU devices
+ thing-type.shelly.shellypblubutton.description = Shelly BLU Button
+ thing-type.shelly.shellybludw.description = Shelly BLU Door/Window Sensor
 # thing config - shellydevice
 thing-type.config.shelly.deviceIp.label = IP Address
 thing-type.config.shelly.deviceIp.description = IP Address of the Shelly device
+thing-type.config.shelly.deviceAddress.label = MAC Address
+thing-type.config.shelly.deviceAddress.description = MAC Address of the Shelly device
 thing-type.config.shelly.userId.label = User ID
 thing-type.config.shelly.userId.description = User ID for API access
 thing-type.config.shelly.password.label = Password
 thing-type.config.shelly.password.description = Password for API access
 thing-type.config.shelly.updateInterval.label = Status Interval
 thing-type.config.shelly.updateInterval.description = Interval for the device status update
+thing-type.config.shelly.enableBluGateway.label = Enable BLU Gateway Support
+thing-type.config.shelly.enableBluGateway.description = Enables BLU Gateway support incl- auto-upload of the required script
 thing-type.config.shelly.eventsButton.label = Button Events
 thing-type.config.shelly.eventsButton.description = Activates the Button Action URLS
 thing-type.config.shelly.eventsPush.label = Push Events
@@ -287,8 +287,8 @@ channel-type.shelly.meterAccuWatts.label = Accumulated Power Consumption
 channel-type.shelly.meterAccuWatts.description = Accumulated Power Consumption in Watts of the device (including all meters)
 channel-type.shelly.meterAccuTotal.label = Accumulated Total Power
 channel-type.shelly.meterAccuTotal.description = Accumulated Total Power in kW/h of the device (including all meters)
-channel-type.shelly.meterAccuReturned.label = Accumulated Returned Power
-channel-type.shelly.meterAccuReturned.description = Accumulated Returned Power in kW/h of the device (including all meters)
+channel-type.shelly.meterAccuReturned.label = Accumulated Apparent Power
+channel-type.shelly.meterAccuReturned.description = Accumulated Apparent Power in kW/h of the device (including all meters)
 channel-type.shelly.meterReactive.label = Reactive Energy
 channel-type.shelly.meterReactive.description = Instantaneous reactive power in Watts (W)
 channel-type.shelly.lastPower1.label = Last Power
@@ -450,6 +450,8 @@ channel-type.shelly.senseKey.label = IR Key to Send
 channel-type.shelly.senseKey.description = Send a defined key code
 channel-type.shelly.deviceName.label = Device Name
 channel-type.shelly.deviceName.description = Symbolic Device Name as configured in the Shelly App
+channel-type.shelly.gatewayDevice.label = Gateway Device
+channel-type.shelly.gatewayDevice.description = Last Shelly Device, which forwarded the event
 channel-type.shelly.uptime.label = Uptime
 channel-type.shelly.uptime.description = Number of seconds since the device was powered up
 channel-type.shelly.heartBeat.label = Last Heartbeat
index 2595cbb5b28d3395a4fa948424f0ad9a9ea30295..0b6f67717e5b17af2c311799e639c8c320735abd 100644 (file)
                <state readOnly="true">
                </state>
        </channel-type>
+       <channel-type id="gatewayDevice" advanced="true">
+               <item-type>String</item-type>
+               <label>@text/channel-type.shelly.gatewayDevice.label</label>
+               <description>@text/channel-type.shelly.gatewayDevice.description</description>
+               <state readOnly="true">
+               </state>
+       </channel-type>
        <channel-type id="calibrated" advanced="true">
                <item-type>Switch</item-type>
                <label>@text/channel-type.shelly.calibrated.label</label>
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/shellyBlu_sensor.xml b/bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/thing/shellyBlu_sensor.xml
new file mode 100644 (file)
index 0000000..c3c8daa
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="shelly"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="shellyblubutton">
+               <label>Shelly BLU Button</label>
+               <description>@text/thing-type.shelly.shellyblubutton.description</description>
+               <category>WallSwitch</category>
+               <channel-groups>
+                       <channel-group id="status" typeId="buttonState"/>
+                       <channel-group id="battery" typeId="batteryStatus"/>
+                       <channel-group id="device" typeId="deviceStatus"/>
+               </channel-groups>
+
+               <representation-property>serviceName</representation-property>
+               <config-description-ref uri="thing-type:shelly:blubattery"/>
+       </thing-type>
+
+       <thing-type id="shellybludw">
+               <label>Shelly BLU Door/Window</label>
+               <description>@text/thing-type.shelly.shellybludw.description</description>
+               <category>Sensor</category>
+               <channel-groups>
+                       <channel-group id="sensors" typeId="sensorData"/>
+                       <channel-group id="battery" typeId="batteryStatus"/>
+                       <channel-group id="device" typeId="deviceStatus"/>
+               </channel-groups>
+
+               <representation-property>serviceName</representation-property>
+               <config-description-ref uri="thing-type:shelly:blubattery"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.shelly/src/main/resources/scripts/oh-blu-scanner.js b/bundles/org.openhab.binding.shelly/src/main/resources/scripts/oh-blu-scanner.js
new file mode 100644 (file)
index 0000000..a7b7389
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * This script uses the BLE scan functionality in scripting to pass scan reults to openHAB
+ * Supported BLU Devices: SBBT , SBDW
+ */
+
+let ALLTERCO_DEVICE_NAME_PREFIX = ["SBBT", "SBDW"];
+let ALLTERCO_MFD_ID_STR = "0ba9";
+let BTHOME_SVC_ID_STR = "fcd2";
+
+let ALLTERCO_MFD_ID = JSON.parse("0x" + ALLTERCO_MFD_ID_STR);
+let BTHOME_SVC_ID = JSON.parse("0x" + BTHOME_SVC_ID_STR);
+let SCAN_DURATION = BLE.Scanner.INFINITE_SCAN;
+
+let SHELLY_BLU_CACHE = {};
+let LAST_PID = {};
+
+let uint8 = 0;
+let int8 = 1;
+let uint16 = 2;
+let int16 = 3;
+let uint24 = 4;
+let int24 = 5;
+
+let BTH = [];
+BTH[0x00] = { n: "pid", t: uint8 };
+BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
+BTH[0x05] = { n: "Illuminance", t: uint24, f: 0.01 };
+BTH[0x1a] = { n: "Door", t: uint8 };
+BTH[0x20] = { n: "Moisture", t: uint8 };
+BTH[0x2d] = { n: "Window", t: uint8 };
+BTH[0x3a] = { n: "Button", t: uint8 };
+BTH[0x3f] = { n: "Rotation", t: int16, f: 0.1 };
+
+function getByteSize(type) {
+  if (type === uint8 || type === int8) return 1;
+  if (type === uint16 || type === int16) return 2;
+  if (type === uint24 || type === int24) return 3;
+  //impossible as advertisements are much smaller;
+  return 255;
+}
+
+let BTHomeDecoder = {
+  utoi: function (num, bitsz) {
+    let mask = 1 << (bitsz - 1);
+    return num & mask ? num - (1 << bitsz) : num;
+  },
+  getUInt8: function (buffer) {
+    return buffer.at(0);
+  },
+  getInt8: function (buffer) {
+    return this.utoi(this.getUInt8(buffer), 8);
+  },
+  getUInt16LE: function (buffer) {
+    return 0xffff & ((buffer.at(1) << 8) | buffer.at(0));
+  },
+  getInt16LE: function (buffer) {
+    return this.utoi(this.getUInt16LE(buffer), 16);
+  },
+  getUInt24LE: function (buffer) {
+    return (
+      0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0))
+    );
+  },
+  getInt24LE: function (buffer) {
+    return this.utoi(this.getUInt24LE(buffer), 24);
+  },
+  getBufValue: function (type, buffer) {
+    if (buffer.length < getByteSize(type)) return null;
+    let res = null;
+    if (type === uint8) res = this.getUInt8(buffer);
+    if (type === int8) res = this.getInt8(buffer);
+    if (type === uint16) res = this.getUInt16LE(buffer);
+    if (type === int16) res = this.getInt16LE(buffer);
+    if (type === uint24) res = this.getUInt24LE(buffer);
+    if (type === int24) res = this.getInt24LE(buffer);
+    return res;
+  },
+  unpack: function (buffer) {
+    // beacons might not provide BTH service data
+    if (typeof buffer !== "string" || buffer.length === 0) return null;
+    let result = {};
+    let _dib = buffer.at(0);
+    result["encryption"] = _dib & 0x1 ? true : false;
+    result["BTHome_version"] = _dib >> 5;
+    if (result["BTHome_version"] !== 2) return null;
+    //Can not handle encrypted data
+    if (result["encryption"]) return result;
+    buffer = buffer.slice(1);
+
+    let _bth;
+    let _value;
+    while (buffer.length > 0) {
+      _bth = BTH[buffer.at(0)];
+      if (_bth === "undefined") {
+        console.log("BTH: unknown type");
+        break;
+      }
+      buffer = buffer.slice(1);
+      _value = this.getBufValue(_bth.t, buffer);
+      if (_value === null) break;
+      if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
+      result[_bth.n] = _value;
+      buffer = buffer.slice(getByteSize(_bth.t));
+    }
+    return result;
+  },
+};
+
+let ShellyBLUParser = {
+  getData: function (res) {
+    let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
+    result.addr = res.addr;
+    result.rssi = res.rssi;
+    return result;
+  },
+};
+
+function scanCB(ev, res) {
+  if (ev !== BLE.Scanner.SCAN_RESULT) return;
+  // skip if there is no service_data member
+  if (typeof res.service_data === 'undefined' || typeof res.service_data[BTHOME_SVC_ID_STR] === 'undefined') return;
+  // skip if we have already found this device
+  if (typeof SHELLY_BLU_CACHE[res.addr] === 'undefined') {
+   if (typeof res.local_name === "undefined") console.log("res.local_name undefined")
+   if (typeof res.local_name !== 'string') return;
+    let shellyBluNameIdx = 0; 
+    for (shellyBluNameIdx in ALLTERCO_DEVICE_NAME_PREFIX) {
+      if (res.local_name.indexOf(ALLTERCO_DEVICE_NAME_PREFIX[shellyBluNameIdx]) === 0) {
+        console.log('New device found: address=', res.addr, ', name=', res.local_name);
+        Shelly.emitEvent("oh-blu.scan_result", {"addr":res.addr, "name":res.local_name, "rssi":res.rssi, "tx_power":res.tx_power_level});
+        SHELLY_BLU_CACHE[res.addr] = res.local_name;
+        }
+    }
+  }
+  
+  let BTHparsed = ShellyBLUParser.getData(res); // skip if parsing failed
+    if (BTHparsed === null) {
+    console.log("Failed to parse BTH data");
+    return;
+  }
+  
+  // skip, we are deduping results
+  if (typeof LAST_PID[res.addr] === 'undefined' ||
+      BTHparsed.pid !== LAST_PID[res.addr]) {
+    Shelly.emitEvent("oh-blu.data", BTHparsed);
+    LAST_PID[res.addr] = BTHparsed.pid;
+  }
+}
+
+// retry several times to start the scanner if script was started before
+// BLE infrastructure was up in the Shelly
+function startBLEScan() {
+  let bleScanSuccess = BLE.Scanner.Start({ duration_ms: SCAN_DURATION, active: true }, scanCB);
+  if( bleScanSuccess === false ) {
+    Timer.set(1000, false, startBLEScan);
+  } else {
+    console.log('Success: OH-BLU Event Gateway running');
+  }
+}
+
+let BLEConfig = Shelly.getComponentConfig('ble');
+if(BLEConfig.enable === false) {
+  console.log('Error: BLE not enabled');
+} else {
+  Timer.set(1000, false, startBLEScan);
+}
+  
\ No newline at end of file