]> git.basschouten.com Git - openhab-addons.git/commitdiff
This PR is just to reduce the delta before I create the PR for Plus/Pro (#13176)
authorMarkus Michels <markus7017@gmail.com>
Fri, 29 Jul 2022 17:48:10 +0000 (19:48 +0200)
committerGitHub <noreply@github.com>
Fri, 29 Jul 2022 17:48:10 +0000 (19:48 +0200)
support. In general Gen1 and Gen2 devices have a total different API so
I spliced the API code in version/gen 1 and 2. This means restructuring
the classes (e.h. api+coap became api+api1+api2) and therefor dummy
changes in the code, e.g. import statements. This creates a lot of
overhead and separating those "dummy changes" simplifies merging the
actual PR.

Signed-off-by: Markus Michels <markus7017@gmail.com>
44 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/ShellyApiInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiResult.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyDeviceProfile.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion2.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapJSonDTO.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapListener.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapServer.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java [new file with mode: 0644]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTInterface.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion1.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapJSonDTO.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapListener.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapServer.java [deleted file]
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyDiscoveryParticipant.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/discovery/ShellyThingCreator.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyBaseHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyColorUtils.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyComponents.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyLightHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyProtectedHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyRelayHandler.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/handler/ShellyThingInterface.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerActionPage.java
bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/manager/ShellyManagerOtaPage.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/java/org/openhab/binding/shelly/internal/util/ShellyUtils.java
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/binding/binding.xml
bundles/org.openhab.binding.shelly/src/main/resources/OH-INF/i18n/shelly.properties

index 1f686d63a32c25ff4f3cc60384a5c6cfb00767db..e4d3ffa5bd753cda73f22ca003687b77359253c7 100644 (file)
@@ -27,6 +27,8 @@ Also check out the [Shelly Manager](doc/ShellyManager.md), which
 
 ## Supported Devices
 
+### Generation 1:
+
 | thing-type         | Model                                                  | Vendor ID |
 |--------------------|--------------------------------------------------------|-----------|
 | shelly1            | Shelly 1 Single Relay Switch                           | SHSW-1    |
@@ -745,22 +747,28 @@ Using the Thing configuration option `brightnessAutoOn` you could decide if the
 `true`:  Brightness will be set and device output is powered = light turns on with the new brightness
 `false`: Brightness will be set, but output stays unchanged so light will not be switched on when it's currently off.
 
-### Shelly RGBW2 in White Mode (thing-type: shellyrgbw2-color)
+### Shelly RGBW2 in Color Mode (thing-type: shellyrgbw2-color)
 
 |Group     |Channel      |Type     |read-only|Description                                                            |
 |----------|-------------|---------|---------|-----------------------------------------------------------------------|
 |control   |power        |Switch   |r/w      |Switch light ON/OFF                                                    |
-|          |input        |Switch   |yes      |State of Input                                                         |
-|          |autoOn       |Number   |r/w      |Sets a  timer to turn the device ON after every OFF; in sec            |
-|          |autoOff      |Number   |r/w      |Sets a  timer to turn the device OFF after every ON: in sec            |
+|          |autoOn       |Number   |r/w      |Sets a  timer to turn the device ON after every OFF command; in seconds|
+|          |autoOff      |Number   |r/w      |Sets a  timer to turn the device OFF after every ON command; in seconds|
 |          |timerActive  |Switch   |yes      |ON: An auto-on/off timer is active                                     |
-|color     |             |         |         |Color settings: only valid in COLOR mode                               |
-|          |hsb          |HSB      |r/w      |Represents the color picker (HSBType), control r/g/b, but not white    |
-|meter     |currentWatts |Number   |yes      |Current power consumption in Watts (all channels)                      |
-
-Please note that the settings of channel group color are only valid in color mode and vice versa for white mode.
-The current firmware doesn't support the timestamp report for the meters. 
-The binding emulates this by using the system time on every update.
+|color     |hsb          |HSB      |r/w      |Represents the color picker (HSBType), control r/g/b, bight not white  |
+|          |full         |String   |r/w      |Set Red / Green / Blue / Yellow / White mode and switch mode           |
+|          |             |         |r/w      |Valid settings: "red", "green", "blue", "yellow", "white" or "r,g,b,w" | 
+|          |red          |Dimmer   |r/w      |Red brightness: 0..100% or 0..255 (control only the red channel)       |
+|          |green        |Dimmer   |r/w      |Green brightness: 0..100% or 0..255 (control only the green channel)   |
+|          |blue         |Dimmer   |r/w      |Blue brightness: 0..100% or 0..255 (control only the blue channel)     |
+|          |white        |Dimmer   |r/w      |White brightness: 0..100% or 0..255 (control only the white channel)   |
+|          |gain         |Dimmer   |r/w      |Gain setting: 0..100%     or 0..100                                    |
+|          |effect       |Number   |r/w      |Puts the light into effect mode: 0..3)                                 |
+|          |             |         |         |0=No effect, 1=Meteor Shower, 2=Gradual Change, 3=Flash                |
+|meter     |currentWatts |Number   |yes      |Current power consumption in Watts                                     |
+|          |lastPower1   |Number   |yes      |Energy consumption for a round minute, 1 minute  ago                   |
+|          |totalKWH     |Number   |yes      |Total energy consumption in kWh since the device powered up (resets on restart)|
+|          |lastUpdate   |DateTime |yes      |Timestamp of the last measurement                                      |
 
 ### Shelly RGBW2 in White Mode (thing-type: shellyrgbw2-white)
 
index e9efbaffeb942af194ac7d3905f33d3094b5c477..2c0e00f10a06f248347968dac24b7c914037b650 100755 (executable)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.shelly.internal;
 
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
+
 import java.util.Collections;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -33,174 +35,6 @@ public class ShellyBindingConstants {
     public static final String BINDING_ID = "shelly";
     public static final String SYSTEM_ID = "system";
 
-    // Type names
-    public static final String THING_TYPE_SHELLY1_STR = "shelly1";
-    public static final String THING_TYPE_SHELLY1L_STR = "shelly1l";
-    public static final String THING_TYPE_SHELLY1PM_STR = "shelly1pm";
-    public static final String THING_TYPE_SHELLYEM_STR = "shellyem";
-    public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
-    public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
-    public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
-    public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
-    public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
-    public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
-    public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
-    public static final String THING_TYPE_SHELLY4PRO_STR = "shelly4pro";
-    public static final String THING_TYPE_SHELLYPLUG_STR = "shellyplug";
-    public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs";
-    public static final String THING_TYPE_SHELLYPLUGU1_STR = "shellyplugu1"; // Shely Plug US
-    public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer";
-    public static final String THING_TYPE_SHELLYDIMMER2_STR = "shellydimmer2";
-    public static final String THING_TYPE_SHELLYIX3_STR = "shellyix3";
-    public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb";
-    public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo";
-    public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellyvintage";
-    public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2";
-    public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-color";
-    public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-white";
-    public static final String THING_TYPE_SHELLYDUORGBW_STR = "shellycolorbulb";
-    public static final String THING_TYPE_SHELLYHT_STR = "shellyht";
-    public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke";
-    public static final String THING_TYPE_SHELLYGAS_STR = "shellygas";
-    public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood";
-    public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw";
-    public static final String THING_TYPE_SHELLYDOORWIN2_STR = "shellydw2";
-    public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye";
-    public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense";
-    public static final String THING_TYPE_SHELLYTRV_STR = "shellytrv";
-    public static final String THING_TYPE_SHELLYMOTION_STR = "shellymotion";
-    public static final String THING_TYPE_SHELLYMOTION2_STR = "shellymotion2";
-    public static final String THING_TYPE_SHELLYBUTTON1_STR = "shellybutton1";
-    public static final String THING_TYPE_SHELLYBUTTON2_STR = "shellybutton2";
-    public static final String THING_TYPE_SHELLYUNI_STR = "shellyuni";
-
-    // Shelly Plus Seriens
-    public static final String THING_TYPE_SHELLYPLUS1_STR = "shellyplus1";
-    public static final String THING_TYPE_SHELLYPLUS1PM_STR = "shellyplus1pm";
-    public static final String THING_TYPE_SHELLYPLUS2PM_RELAY_STR = "shellyplus2pm-relay";
-    public static final String THING_TYPE_SHELLYPLUS2PM_ROLLER_STR = "shellyplus2pm-roller";
-    public static final String THING_TYPE_SHELLYPLUSI4_STR = "shellyplusi4";
-    public static final String THING_TYPE_SHELLYPLUSHT_STR = "shellyplusht";
-    public static final String THING_TYPE_SHELLYPLUSPLUGUS_STR = "shellyplusplugus";
-
-    // Shelly Pro Series
-    public static final String THING_TYPE_SHELLYPRO1_STR = "shellypro1";
-    public static final String THING_TYPE_SHELLYPRO1PM_STR = "shellypro1pm";
-    public static final String THING_TYPE_SHELLYPRO2_RELAY_STR = "shellypro2-relay";
-    public static final String THING_TYPE_SHELLYPRO2_ROLLER_STR = "shellypro2-roller";
-    public static final String THING_TYPE_SHELLYPRO2PM_RELAY_STR = "shellypro2pm-relay";
-    public static final String THING_TYPE_SHELLYPRO2PM_ROLLER_STR = "shellypro2pm-roller";
-    public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm";
-
-    public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
-    public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
-
-    // Device Types
-    public static final String SHELLYDT_1 = "SHSW-1";
-    public static final String SHELLYDT_1PM = "SHSW-PM";
-    public static final String SHELLYDT_1L = "SHSW-L";
-    public static final String SHELLYDT_SHPLG = "SHPLG-1";
-    public static final String SHELLYDT_SHPLG_S = "SHPLG-S";
-    public static final String SHELLYDT_SHPLG_U1 = "SHPLG-U1";
-    public static final String SHELLYDT_SHELLY2 = "SHSW-21";
-    public static final String SHELLYDT_SHELLY25 = "SHSW-25";
-    public static final String SHELLYDT_SHPRO = "SHSW-44";
-    public static final String SHELLYDT_EM = "SHEM";
-    public static final String SHELLYDT_3EM = "SHEM-3";
-    public static final String SHELLYDT_HT = "SHHT-1";
-    public static final String SHELLYDT_DW = "SHDW-1";
-    public static final String SHELLYDT_DW2 = "SHDW-2";
-    public static final String SHELLYDT_SENSE = "SHSEN-1";
-    public static final String SHELLYDT_MOTION = "SHMOS-01";
-    public static final String SHELLYDT_MOTION2 = "SHMOS-02";
-    public static final String SHELLYDT_GAS = "SHGS-1";
-    public static final String SHELLYDT_DIMMER = "SHDM-1";
-    public static final String SHELLYDT_DIMMER2 = "SHDM-2";
-    public static final String SHELLYDT_IX3 = "SHIX3-1";
-    public static final String SHELLYDT_BULB = "SHBLB-1";
-    public static final String SHELLYDT_DUO = "SHBDUO-1";
-    public static final String SHELLYDT_DUORGBW = "SHCB-1";
-    public static final String SHELLYDT_VINTAGE = "SHVIN-1";
-    public static final String SHELLYDT_RGBW2 = "SHRGBW2";
-    public static final String SHELLYDT_BUTTON1 = "SHBTN-1";
-    public static final String SHELLYDT_BUTTON2 = "SHBTN-2";
-    public static final String SHELLYDT_UNI = "SHUNI-1";
-    public static final String SHELLYDT_TRV = "SHTRV-01";
-
-    // Shelly Plus Series
-    public static final String SHELLYDT_PLUS1 = "SNSW-001X16EU";
-    public static final String SHELLYDT_PLUS1PM = "SNSW-001P16EU";
-    public static final String SHELLYDT_PLUS2PM_RELAY = "SNSW-002P16EU-relay";
-    public static final String SHELLYDT_PLUS2PM_ROLLER = "SNSW-002P16EU-roller";
-    public static final String SHELLYDT_PLUSPLUGUS = "SNPL-00116US";
-    public static final String SHELLYDT_PLUSI4 = "SNSN-0024X";
-    public static final String SHELLYDT_PLUSHT = "SNSN-0013A";
-
-    // Shelly Pro Series
-    public static final String SHELLYDT_PRO1 = "SPSW-001XE16EU";
-    public static final String SHELLYDT_PRO1PM = "SPSW-001PE16EU";
-    public static final String SHELLYDT_PRO2_RELAY = "SPSW-002XE16EU-relay";
-    public static final String SHELLYDT_PRO2_ROLLER = "SPSW-002XE16EU-roller";
-    public static final String SHELLYDT_PRO2PM_RELAY = "SPSW-002PE16EU-relay";
-    public static final String SHELLYDT_PRO2PM_ROLLER = "SPSW-002PE16EU-roller";
-    public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
-
-    // List of all Thing Type UIDs
-    public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY1L = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1L_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PM_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLY2_RELAY_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLY2_ROLLER_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLY25_RELAY_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLY25_ROLLER_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLY4PRO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY4PRO_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYPLUG = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUG_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYPLUGS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUGS_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYPLUGU1 = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYPLUGU1_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYUNI = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYUNI_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYDIMMER_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYDIMMER2 = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYDIMMER2_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYIX3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYIX3_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYVINTAGE_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYDUORGBW = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYDUORGBW_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYGAS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYGAS_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYDOORWIN_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN2 = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYDOORWIN2_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYTRV = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYTRV_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYBUTTON1 = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYBUTTON1_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYBUTTON2 = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYBUTTON2_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLMOTION = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYMOTION_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYRGBW2_COLOR_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_WHITE = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYRGBW2_WHITE_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYPROTECTED = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYPROTECTED_STR);
-    public static final ThingTypeUID THING_TYPE_SHELLYUNKNOWN = new ThingTypeUID(BINDING_ID,
-            THING_TYPE_SHELLYUNKNOWN_STR);
-
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
             .unmodifiableSet(Stream.of(THING_TYPE_SHELLY1, THING_TYPE_SHELLY1L, THING_TYPE_SHELLY1PM,
                     THING_TYPE_SHELLYEM, THING_TYPE_SHELLY3EM, THING_TYPE_SHELLY2_RELAY, THING_TYPE_SHELLY2_ROLLER,
index 4f5bb53a78e8cece8241ce3bcbd722f9b64bb048..a2cab8c4fca83738308ad78062c2d2fbc03b5f5e 100755 (executable)
@@ -12,7 +12,7 @@
  */
 package org.openhab.binding.shelly.internal;
 
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -21,7 +21,7 @@ import java.util.Set;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+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.ShellyLightHandler;
@@ -62,7 +62,7 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
     private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
     private final HttpClient httpClient;
     private final ShellyTranslationProvider messages;
-    private final ShellyCoapServer coapServer;
+    private final Shelly1CoapServer coapServer;
 
     private final ShellyThingTable thingTable;
     private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
@@ -81,7 +81,6 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
             @Reference ShellyTranslationProvider translationProvider, @Reference ShellyThingTable thingTable,
             @Reference HttpClientFactory httpClientFactory, ComponentContext componentContext,
             Map<String, Object> configProperties) {
-        logger.debug("Activate Shelly HandlerFactory");
         super.activate(componentContext);
         messages = translationProvider;
         // Save bindingConfig & pass it to all registered listeners
@@ -103,10 +102,7 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
         logger.debug("Using OH HTTP port {}", httpPort);
 
         this.thingTable = thingTable;
-        this.coapServer = new ShellyCoapServer();
-
-        // Promote Shelly Manager usage
-        logger.info("{}", messages.get("status.managerstarted", localIP, httpPort));
+        this.coapServer = new Shelly1CoapServer();
     }
 
     @Override
index a7238a8b732dbaa743a77c860a212c6a17112db8..e267de7e82977bfdb28e98da93ef3ce5ffd33876 100644 (file)
@@ -15,15 +15,15 @@ package org.openhab.binding.shelly.internal.api;
 import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 
 /**
@@ -47,11 +47,11 @@ public interface ShellyApiInterface {
 
     public void setSleepTime(int value) throws ShellyApiException;
 
-    public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException;
+    public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException;
 
     public void setRelayTurn(int id, String turnMode) throws ShellyApiException;
 
-    public ShellyControlRoller getRollerStatus(int rollerIndex) throws ShellyApiException;
+    public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException;
 
     public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException;
 
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyApiJsonDTO.java
deleted file mode 100644 (file)
index 3915892..0000000
+++ /dev/null
@@ -1,1192 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.api;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
-import org.openhab.core.thing.CommonTriggerEvents;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * The {@link ShellyApiJsonDTO} is used for the JSon/GSon mapping
- *
- * @author Markus Michels - Initial contribution
- */
-public class ShellyApiJsonDTO {
-    public static final String SHELLY_NULL_URL = "null";
-    public static final String SHELLY_URL_DEVINFO = "/shelly";
-    public static final String SHELLY_URL_STATUS = "/status";
-    public static final String SHELLY_URL_SETTINGS = "/settings";
-    public static final String SHELLY_URL_SETTINGS_AP = "/settings/ap";
-    public static final String SHELLY_URL_SETTINGS_STA = "/settings/sta";
-    public static final String SHELLY_URL_SETTINGS_LOGIN = "/settings/sta";
-    public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud";
-    public static final String SHELLY_URL_LIST_IR = "/ir/list";
-    public static final String SHELLY_URL_SEND_IR = "/ir/emit";
-    public static final String SHELLY_URL_RESTART = "/reboot";
-
-    public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay";
-    public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay";
-    public static final String SHELLY_URL_CONTROL_RELEAY = "/relay";
-
-    public static final String SHELLY_URL_SETTINGS_EMETER = "/settings/emeter";
-    public static final String SHELLY_URL_STATUS_EMETER = "/emeter";
-    public static final String SHELLY_URL_DATA_EMETER = "/emeter/{0}/em_data.csv";
-
-    public static final String SHELLY_URL_CONTROL_ROLLER = "/roller";
-    public static final String SHELLY_URL_SETTINGS_ROLLER = "/settings/roller";
-
-    public static final String SHELLY_URL_SETTINGS_LIGHT = "/settings/light";
-    public static final String SHELLY_URL_STATUS_LIGHT = "/light";
-    public static final String SHELLY_URL_CONTROL_LIGHT = "/light";
-
-    public static final String SHELLY_URL_SETTINGS_DIMMER = "/settings/light";
-
-    // Wakeup reasons
-    public static final String SHELLY_WAKEUPT_SENSOR = "SENSOR"; // new sensordata
-    public static final String SHELLY_WAKEUPT_PERIODIC = "PERIODIC"; // periodic wakeup
-    public static final String SHELLY_WAKEUPT_BUTTON = "BUTTON"; // button pressed
-    public static final String SHELLY_WAKEUPT_POWERON = "POWERON"; // device powered up
-    public static final String SHELLY_WAKEUPT_EXT_POWER = "EXT_POWER"; // charger connected
-    public static final String SHELLY_WAKEUPT_UNKNOWN = "UNKNOWN"; // other event
-
-    //
-    // Action URLs according to the device type
-    //
-    public static final String SHELLY_EVENTURL_SUFFIX = "_url";
-
-    // Relay
-    public static final String SHELLY_EVENT_BTN_ON = "btn_on";
-    public static final String SHELLY_EVENT_BTN_OFF = "btn_off";
-    public static final String SHELLY_EVENT_OUT_ON = "out_on";
-    public static final String SHELLY_EVENT_OUT_OFF = "out_off";
-    public static final String SHELLY_EVENT_SHORTPUSH = "shortpush";
-    public static final String SHELLY_EVENT_LONGPUSH = "longpush";
-    // Button
-    public static final String SHELLY_EVENT_DOUBLE_SHORTPUSH = "double_shortpush";
-    public static final String SHELLY_EVENT_TRIPLE_SHORTPUSH = "triple_shortpush";
-    public static final String SHELLY_EVENT_SHORT_LONGTPUSH = "shortpush_longpush";
-    public static final String SHELLY_EVENT_LONG_SHORTPUSH = "longpush_shortpush";
-
-    // Dimmer
-    public static final String SHELLY_EVENT_BTN1_ON = "btn1_on";
-    public static final String SHELLY_EVENT_BTN1_OFF = "btn1_off";
-    public static final String SHELLY_EVENT_BTN2_ON = "btn2_on";
-    public static final String SHELLY_EVENT_BTN2_OFF = "btn2_off";
-    public static final String SHELLY_EVENT_SHORTPUSH1 = "btn1_shortpush";
-    public static final String SHELLY_EVENT_LONGPUSH1 = "btn1_longpush";
-    public static final String SHELLY_EVENT_SHORTPUSH2 = "btn2_shortpush";
-    public static final String SHELLY_EVENT_LONGPUSH2 = "btn2_longpush";
-
-    // Roller
-    public static final String SHELLY_EVENT_ROLLER_OPEN = "roller_open";
-    public static final String SHELLY_EVENT_ROLLER_CLOSE = "roller_close";
-    public static final String SHELLY_EVENT_ROLLER_STOP = "roller_stop";
-    public static final String SHELLY_EVENT_ROLLER_CALIB = "roller_calibrating";
-
-    // Roller states
-    public static final String SHELLY_RSTATE_OPEN = "open";
-    public static final String SHELLY_RSTATE_STOP = "stop";
-    public static final String SHELLY_RSTATE_CLOSE = "close";
-
-    // Sensors
-    public static final String SHELLY_EVENT_SENSORREPORT = "report";
-    public static final String SHELLY_EVENT_DARK = "dark";
-    public static final String SHELLY_EVENT_TWILIGHT = "twilight";
-    public static final String SHELLY_EVENT_BRIGHT = "bright";
-    public static final String SHELLY_EVENT_FLOOD_DETECTED = "flood_detected";
-    public static final String SHELLY_EVENT_FLOOD_GONE = "flood_gone";
-    public static final String SHELLY_EVENT_VIBRATION = "vibration"; // DW 1.6.5+
-    public static final String SHELLY_EVENT_OPEN = "open"; // DW 1.6.5+
-    public static final String SHELLY_EVENT_CLOSE = "close"; // DW 1.6.5+
-    public static final String SHELLY_EVENT_TEMP_OVER = "temp_over"; // FW 1.7
-    public static final String SHELLY_EVENT_TEMP_UNDER = "temp_under"; // FW 1.7
-
-    // Gas
-    public static final String SHELLY_EVENT_ALARM_MILD = "alarm_mild"; // DW 1.7+
-    public static final String SHELLY_EVENT_ALARM_HEAVY = "alarm_heavy"; // DW 1.7+
-    public static final String SHELLY_EVENT_ALARM_OFF = "alarm_off"; // DW 1.7+
-
-    //
-    // API values
-    //
-    public static final double SHELLY_API_INVTEMP = -999.0;
-
-    public static final String SHELLY_BTNT_MOMENTARY = "momentary";
-    public static final String SHELLY_BTNT_MOM_ON_RELEASE = "momentary_on_release";
-    public static final String SHELLY_BTNT_ONE_BUTTON = "one_button";
-    public static final String SHELLY_BTNT_TWO_BUTTON = "dual_button";
-    public static final String SHELLY_BTNT_TOGGLE = "toggle";
-    public static final String SHELLY_BTNT_EDGE = "edge";
-    public static final String SHELLY_BTNT_DETACHED = "detached";
-
-    public static final String SHELLY_STATE_LAST = "last";
-    public static final String SHELLY_STATE_STOP = "stop";
-
-    public static final String SHELLY_INP_MODE_OPENCLOSE = "openclose";
-    public static final String SHELLY_INP_MODE_ONEBUTTON = "onebutton";
-
-    public static final String SHELLY_OBSTMODE_DISABLED = "disabled";
-    public static final String SHELLY_SAFETYM_WHILEOPENING = "while_opening";
-
-    public static final String SHELLY_ALWD_TRIGGER_NONE = "none";
-    public static final String SHELLY_ALWD_ROLLER_TURN_OPEN = "open";
-    public static final String SHELLY_ALWD_ROLLER_TURN_CLOSE = "close";
-    public static final String SHELLY_ALWD_ROLLER_TURN_STOP = "stop";
-
-    // API Error Codes
-    public static final String SHELLY_APIERR_UNAUTHORIZED = "Unauthorized";
-    public static final String SHELLY_APIERR_TIMEOUT = "Timeout";
-    public static final String SHELLY_APIERR_NOT_CALIBRATED = "Not calibrated!";
-
-    // API device types / properties
-    public static final String SHELLY_CLASS_RELAY = "relay"; // Relay: relay mode
-    public static final String SHELLY_CLASS_ROLLER = "roller"; // Relay: roller mode
-    public static final String SHELLY_CLASS_LIGHT = "light"; // Bulb: color mode
-    public static final String SHELLY_CLASS_EMETER = "emeter"; // EM/EM3: emeter
-
-    public static final String SHELLY_API_ON = "on";
-    public static final String SHELLY_API_OFF = "off";
-    public static final String SHELLY_API_TRUE = "true";
-    public static final String SHELLY_API_FALSE = "false";
-
-    public static final String SHELLY_API_MODE = "mode";
-    public static final String SHELLY_MODE_RELAY = "relay"; // Relay: relay mode
-    public static final String SHELLY_MODE_ROLLER = "roller"; // Relay: roller mode
-    public static final String SHELLY_MODE_COLOR = "color"; // Bulb/RGBW2: color mode
-    public static final String SHELLY_MODE_WHITE = "white"; // Bulb/RGBW2: white mode
-
-    public static final String SHELLY_LED_STATUS_DISABLE = "led_status_disable";
-    public static final String SHELLY_LED_POWER_DISABLE = "led_power_disable";
-
-    public static final String SHELLY_API_STOPR_NORMAL = "normal";
-    public static final String SHELLY_API_STOPR_SAFETYSW = "safety_switch";
-    public static final String SHELLY_API_STOPR_OBSTACLE = "obstacle";
-    public static final String SHELLY_API_STOPR_OVERPOWER = "overpower";
-
-    public static final String SHELLY_TIMER_AUTOON = "auto_on";
-    public static final String SHELLY_TIMER_AUTOOFF = "auto_off";
-    public static final String SHELLY_TIMER_ACTIVE = "has_timer";
-
-    public static final String SHELLY_LIGHT_TURN = "turn";
-    public static final String SHELLY_LIGHT_DEFSTATE = "def_state";
-    public static final String SHELLY_LIGHTTIMER = "timer";
-
-    public static final String SHELLY_COLOR_RED = "red";
-    public static final String SHELLY_COLOR_BLUE = "blue";
-    public static final String SHELLY_COLOR_GREEN = "green";
-    public static final String SHELLY_COLOR_YELLOW = "yellow";
-    public static final String SHELLY_COLOR_WHITE = "white";
-    public static final String SHELLY_COLOR_GAIN = "gain";
-    public static final String SHELLY_COLOR_BRIGHTNESS = "brightness";
-    public static final String SHELLY_COLOR_TEMP = "temp";
-    public static final String SHELLY_COLOR_EFFECT = "effect";
-
-    public static final int SHELLY_MIN_ROLLER_POS = 0;
-    public static final int SHELLY_MAX_ROLLER_POS = 100;
-    public static final int SHELLY_MIN_BRIGHTNESS = 0;
-    public static final int SHELLY_MAX_BRIGHTNESS = 100;
-    public static final int SHELLY_MIN_GAIN = 0;
-    public static final int SHELLY_MAX_GAIN = 100;
-    public static final int SHELLY_MIN_COLOR = 0;
-    public static final int SHELLY_MAX_COLOR = 255;
-    public static final int SHELLY_DIM_STEPSIZE = 10;
-
-    // color temperature: 3000 = warm, 4750 = white, 6565 = cold; gain: 0..100
-    public static final int MIN_COLOR_TEMP_BULB = 3000;
-    public static final int MAX_COLOR_TEMP_BULB = 6500;
-    public static final int MIN_COLOR_TEMP_DUO = 2700;
-    public static final int MAX_COLOR_TEMP_DUO = 6500;
-    public static final int COLOR_TEMP_RANGE_BULB = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
-    public static final int COLOR_TEMP_RANGE_DUO = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
-    public static final double MIN_BRIGHTNESS = 0.0;
-    public static final double MAX_BRIGHTNESS = 100.0;
-    public static final double SATURATION_FACTOR = 2.55;
-    public static final double GAIN_FACTOR = SHELLY_MAX_GAIN / 100;
-    public static final double BRIGHTNESS_FACTOR = SHELLY_MAX_BRIGHTNESS / 100;
-
-    // Door/Window
-    public static final String SHELLY_API_ILLUM_DARK = "dark";
-    public static final String SHELLY_API_ILLUM_TWILIGHT = "twilight";
-    public static final String SHELLY_API_ILLUM_BRIGHT = "bright";
-    public static final String SHELLY_API_DWSTATE_OPEN = "open";
-    public static final String SHELLY_API_DWSTATE_CLOSE = "close";
-
-    // Shelly Sense
-    public static final String SHELLY_IR_CODET_STORED = "stored";
-    public static final String SHELLY_IR_CODET_PRONTO = "pronto";
-    public static final String SHELLY_IR_CODET_PRONTO_HEX = "pronto_hex";
-
-    // Bulb/Duo/RGBW2
-    public static final int SHELLY_MIN_EFFECT = 0;
-    public static final int SHELLY_MAX_EFFECT = 6;
-
-    // Button
-    public static final String SHELLY_BTNEVENT_1SHORTPUSH = "S";
-    public static final String SHELLY_BTNEVENT_2SHORTPUSH = "SS";
-    public static final String SHELLY_BTNEVENT_3SHORTPUSH = "SSS";
-    public static final String SHELLY_BTNEVENT_LONGPUSH = "L";
-    public static final String SHELLY_BTNEVENT_SHORTLONGPUSH = "SL";
-    public static final String SHELLY_BTNEVENT_LONGSHORTPUSH = "LS";
-
-    public static final String SHELLY_TEMP_CELSIUS = "C";
-    public static final String SHELLY_TEMP_FAHRENHEIT = "F";
-
-    // Motion
-    public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset
-
-    // TRV
-    public static final int SHELLY_TRV_MIN_TEMP = 5; // < 5: means: lowest (valve fully closed)
-    public static final int SHELLY_TRV_MAX_TEMP = 30; // > 30: means: highest (valve fully open)
-
-    public static final String SHELLY_TRV_MODE_MANUAL = "manual";
-    public static final String SHELLY_TRV_MODE_AUTO = "automatic";
-
-    // CoIoT Multicast setting
-    public static final String SHELLY_COIOT_MCAST = "mcast";
-
-    public static class ShellySettingsDevice {
-        public String type;
-        public String mac;
-        public String hostname;
-        public String fw;
-        public Boolean auth;
-
-        @SerializedName("coiot") // Shelly Motion Multicast Endpoint
-        public String coiot;
-        public Integer longid;
-
-        @SerializedName("num_outputs")
-        public Integer numOutputs;
-        @SerializedName("num_meters")
-        public Integer numMeters;
-        @SerializedName("num_emeters")
-        public Integer numEMeters;
-        @SerializedName("num_rollers")
-        public Integer numRollers;
-    }
-
-    public static class ShellySettingsWiFiAp {
-        public Boolean enabled;
-        public String ssid;
-        public String key;
-    }
-
-    public static class ShellySettingsWiFiNetwork {
-        public Boolean enabled;
-        public String ssid;
-        public Integer rssi;
-
-        @SerializedName("ipv4_method")
-        public String ipv4Method;
-        public String ip;
-        public String gw;
-        public String mask;
-        public String dns;
-    }
-
-    public static class ShellySettingsMqtt {
-        public Boolean enable;
-        public String server;
-        public String user;
-        @SerializedName("reconnect_timeout_max")
-        public Double reconnectTimeoutMax;
-        @SerializedName("reconnect_timeout_min")
-        public Double reconnectTimeoutMin;
-        @SerializedName("clean_session")
-        public Boolean cleanSession;
-        @SerializedName("keep_alive")
-        public Integer keepAlive;
-        @SerializedName("will_topic")
-        public String willTopic;
-        @SerializedName("will_message")
-        public String willMessage;
-        @SerializedName("max_qos")
-        public Integer maxQOS;
-        public Boolean retain;
-        @SerializedName("update_period")
-        public Integer updatePeriod;
-    }
-
-    public static class ShellySettingsCoiot { // FW 1.6+
-        @SerializedName("update_period")
-        public Integer updatePeriod;
-        public Boolean enabled; // Motion 1.0.7: Coap can be disabled
-        public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast
-    }
-
-    public static class ShellyStatusMqtt {
-        public Boolean connected;
-    }
-
-    public static class ShellySettingsSntp {
-        public String server;
-        public Boolean enabled;
-    }
-
-    public static class ShellySettingsLogin {
-        public Boolean enabled;
-        public Boolean unprotected;
-        public String username;
-        public String password;
-    }
-
-    public static class ShellySettingsBuildInfo {
-        @SerializedName("build_id")
-        public String buildId;
-        @SerializedName("build_timestamp")
-        public String buildTimestamp;
-        @SerializedName("build_version")
-        public String buildVersion;
-    }
-
-    public static class ShellyStatusCloud {
-        public Boolean enabled;
-        public Boolean connected;
-    }
-
-    public static class ShellySettingsHwInfo {
-        @SerializedName("hw_revision")
-        public String hwRevision;
-        @SerializedName("batch_id")
-        public Integer batchId;
-    }
-
-    public static class ShellySettingsScheduleRules {
-    }
-
-    public static class ShellySettingsRelay {
-        public String name;
-        @SerializedName("default_state")
-        public String defaultState; // Accepted values: off, on, last, switch
-        @SerializedName("btn_type")
-        public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
-        @SerializedName("btn1_type") // Shelly 1L
-        public String btnType1;
-        @SerializedName("btn2_type") // Shelly 1L
-        public String btnType2;
-        @SerializedName("has_timer")
-        public Boolean hasTimer; // Whether a timer is currently armed for this channel
-        @SerializedName("auto_on")
-        public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
-        @SerializedName("auto_off")
-        public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
-        @SerializedName("btn_on_url")
-        public String btnOnUrl; // input is activated
-        @SerializedName("btnOffUrl")
-        public String btnOffUrl; // input is deactivated
-        @SerializedName("out_on_url")
-        public String outOnUrl; // output is activated
-        @SerializedName("out_off_url")
-        public String outOffUrl; // output is deactivated
-        @SerializedName("roller_open_url")
-        public String rollerOpenUrl; // to access when roller reaches open position
-        @SerializedName("roller_close_url")
-        public String rollerCloseUrl; // to access when roller reaches close position
-        @SerializedName("roller_stop_url")
-        public String rollerStopUrl; // to access when roller stopped
-        @SerializedName("longpush_url")
-        public String pushLongUrl; // to access when roller stopped
-        @SerializedName("shortpush_url")
-        public String pushShortUrl; // to access when roller stopped
-
-        // Status information
-        public Boolean ison;
-        public Boolean overpower;
-        @SerializedName("is_valid")
-        public Boolean isValid;
-        @SerializedName("ext_temperature")
-        public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
-        @SerializedName("ext_humidity")
-        public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
-    }
-
-    public static class ShellySettingsDimmer {
-        public String name; // unique name of the device
-        public Boolean ison; // true: output is ON
-        @SerializedName("default_state")
-        public String defaultState; // Accepted values: off, on, last, switch
-        @SerializedName("auto_on")
-        public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
-        @SerializedName("auto_off")
-        public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
-        @SerializedName("btn1_on_url")
-        public String btn1OnUrl; // URL to access when SW input is activated
-        @SerializedName("btn1_off_url")
-        public String btn1OffUrl; // URL to access when SW input is deactivated
-        @SerializedName("btn2_on_url")
-        public String btn2OnUrl; // URL to access when SW input is activated
-        @SerializedName("btn2_off_url")
-        public String btn2OoffUrl; // URL to access when SW input is deactivated
-        @SerializedName("out_on_url")
-        public String outOnUrl; // URL to access when output is activated
-        @SerializedName("out_off_url")
-        public String outOffUrl; // URL to access when output is deactivated
-        @SerializedName("longpush_url")
-        public String pushLongUrl; // long push button event
-        @SerializedName("shortpush_url")
-        public String pushShortUrl; // short push button event
-        @SerializedName("btn_type")
-        public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
-        @SerializedName("btn1_type")
-        public String btnType1; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
-        @SerializedName("btn2_type")
-        public String btnType2; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
-        @SerializedName("swap_inputs")
-        public Integer swapInputs; // 0=no
-    }
-
-    public static class ShellySettingsRoller {
-        public Double maxtime;
-        @SerializedName("maxtime_open")
-        public Double maxtimeOpen;
-        @SerializedName("maxtime_close")
-        public Double maxtimeClose;
-        @SerializedName("default_state")
-        public String defaultState; // see SHELLY_STATE_xxx
-        public Boolean swap;
-        @SerializedName("swap_inputs")
-        public Boolean swapInputs;
-        @SerializedName("input_mode")
-        public String inputMode; // see SHELLY_INP_MODE_OPENCLOSE
-        @SerializedName("button_type")
-        public String buttonType; // // see SHELLY_BTNT_xxx
-        @SerializedName("btn_Reverse")
-        public Integer btnReverse;
-        public String state;
-        public Double power;
-        @SerializedName("is_valid")
-        public Boolean isValid;
-        @SerializedName("safety_switch")
-        public Boolean safetySwitch;
-        @SerializedName("obstacle_mode")
-        public String obstaclMode; // SHELLY_OBSTMODE_
-        @SerializedName("obstacle_action")
-        public String obstacleAction; // see SHELLY_STATE_xxx
-        @SerializedName("obstacle_power")
-        public Integer obstaclePower;
-        @SerializedName("obstacle_delay")
-        public Integer obstacleDelay;
-        @SerializedName("safety_mode")
-        public String safetyMode; // see SHELLY_SAFETYM_xxx
-        @SerializedName("safety_action")
-        public String safetyAction; // see SHELLY_STATE_xxx
-        @SerializedName("safety_allowed_on_trigger")
-        public String safetyAllowedOnTrigger; // see SHELLY_ALWD_TRIGGER_xxx
-        @SerializedName("off_power")
-        public Integer offPower;
-        public Boolean positioning;
-    }
-
-    public static class ShellySettingsRgbwLight {
-        public String name;
-        public Boolean ison; // true: output is ON
-        public Integer brightness;
-        public Integer transition;
-        @SerializedName("default_state")
-        public String defaultState;
-        @SerializedName("auto_on")
-        public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
-        @SerializedName("auto_off")
-        public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
-        public Boolean schedule;
-        @SerializedName("btn_type")
-        public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
-        @SerializedName("btn_reverse")
-        public Integer btnReverse; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
-        @SerializedName("out_on_url")
-        public String outOnUrl; // output is activated
-        @SerializedName("out_off_url")
-        public String outOffUrl; // output is deactivated
-    }
-
-    public static class ShellyFavPos { // FW 1.9.2+ in roller mode
-        public String name;
-        public Integer pos;
-    }
-
-    public static class ShellyInputState {
-        public Integer input;
-
-        // Shelly Button
-        public String event;
-        @SerializedName("event_cnt")
-        public Integer eventCount;
-    }
-
-    public static class ShellySettingsMeter {
-        @SerializedName("is_valid")
-        public Boolean isValid;
-        public Double power;
-        public Double[] counters = { 0.0, 0.0, 0.0 };
-        public Double total;
-        public Long timestamp;
-    }
-
-    public static class ShellySettingsEMeter { // ShellyEM meter
-        @SerializedName("is_valid")
-        public Boolean isValid; // Whether the associated meter is functioning properly
-        public Double power; // Instantaneous power, Watts
-        public Double reactive; // Instantaneous reactive power, Watts
-        public Double voltage; // RMS voltage, Volts
-        public Double total; // Total consumed energy, Wh
-        @SerializedName("total_returned")
-        public Double totalReturned; // Total returned energy, Wh
-
-        public Double pf; // 3EM
-        public Double current; // 3EM
-    }
-
-    public static class ShellySettingsUpdate {
-        public String status;
-        @SerializedName("has_update")
-        public Boolean hasUpdate;
-        @SerializedName("new_version")
-        public String newVersion;
-        @SerializedName("old_version")
-        public String oldVersion;
-        @SerializedName("beta_version")
-        public String betaVersion;
-    }
-
-    public static class ShellySettingsGlobal {
-        // https://shelly-api-docs.shelly.cloud/#shelly1pm-settings
-        public ShellySettingsDevice device;
-        @SerializedName("wifi_ap")
-        public ShellySettingsWiFiAp wifiAp;
-        @SerializedName("wifi_sta")
-        public ShellySettingsWiFiNetwork wifiSta;
-        @SerializedName("wifi_sta1")
-        public ShellySettingsWiFiNetwork wifiSta1;
-        @SerializedName("wifirecovery_reboot_enabled")
-        public Boolean wifiRecoveryReboot; // FW 1.10+
-        @SerializedName("ap_roaming")
-        public ShellyApRoaming apRoaming; // FW 1.10+
-
-        public ShellySettingsMqtt mqtt; // not used for now
-        public ShellySettingsSntp sntp; // not used for now
-        public ShellySettingsCoiot coiot; // Firmware 1.6+
-        public ShellySettingsLogin login;
-        @SerializedName("pin_code")
-        public String pinCode;
-        @SerializedName("coiot_execute_enable")
-        public Boolean coiotExecuteEnable;
-        public String name;
-        public Boolean discoverable; // FW 1.6+
-        public String fw;
-        @SerializedName("build_info")
-        public ShellySettingsBuildInfo buildInfo;
-        public ShellyStatusCloud cloud;
-        @SerializedName("sleep_mode")
-        public ShellySensorSleepMode sleepMode; // FW 1.6
-        @SerializedName("external_power")
-        public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
-        @SerializedName("debug_enable") // FW 1.10+
-        public Boolean debugEnable;
-
-        public String timezone;
-        public Double lat;
-        public Double lng;
-        public Boolean tzautodetect;
-        public String time;
-
-        public ShellySettingsHwInfo hwinfo;
-        public String mode;
-        @SerializedName("max_power")
-        public Double maxPower;
-        public Boolean calibrated;
-
-        public ArrayList<ShellySettingsRelay> relays;
-        public ArrayList<ShellySettingsRoller> rollers;
-        public ArrayList<ShellySettingsDimmer> dimmers;
-        public ArrayList<ShellySettingsRgbwLight> lights;
-        public ArrayList<ShellySettingsEMeter> emeters;
-        public ArrayList<ShellySettingsInput> inputs; // ix3
-        public ArrayList<ShellyThermnostat> thermostats; // TRV
-
-        public Double voltage; // AC voltage for Shelly 2.5
-        @SerializedName("supply_voltage")
-        public Long supplyVoltage; // Shelly 1PM/1L: 0=110V, 1=220V
-
-        @SerializedName("temperature_units")
-        public String temperatureUnits = "C"; // Either'C'or'F'
-
-        @SerializedName("led_status_disable")
-        public Boolean ledStatusDisable; // PlugS only Disable LED indication for network
-                                         // status
-        @SerializedName("led_power_disable")
-        public Boolean ledPowerDisable; // PlugS only Disable LED indication for network
-                                        // status
-        @SerializedName("light_sensor")
-        public String lightSensor; // Sense: sensor type
-        @SerializedName("rain_sensor")
-        public Boolean rainSensor; // Flood: true=in rain mode
-
-        // FW 1.5.7: Door Window
-        @SerializedName("dark_treshold")
-        public Integer darkTreshold; // Illumination definition for "dark" in lux
-        @SerializedName("twilight_treshold")
-        public Integer twiLightTreshold; // Illumination definition for "twilight" in lux
-        @SerializedName("dark_url")
-        public String darkUrl; // URL to report to when luminance <= dark_threshold
-        @SerializedName("twilight_url")
-        public String twiLightUrl; // URL reports when luminance > dark_threshold AND luminance <=
-        @SerializedName("close_url")
-        public String closeUrl; // URL reports when DW contact is closed FW 1.6.5+
-        @SerializedName("vibration_url")
-        public String vibrationUrl; // URL reports when DW detects vibration FW 1.6.5+
-
-        // Gas FW 1.7
-        @SerializedName("set_volume")
-        public Integer volume; // Speaker volume for alarm
-        @SerializedName("alarm_off_url")
-        public String alarmOffUrl; // URL reports when alarm went off
-        @SerializedName("alarm_mild_url")
-        public String alarmMidUrl; // URL reports middle alarm
-        @SerializedName("alarm_heavy_url")
-        public String alarmHeavyfUrl; // URL reports heavy alarm
-
-        // Roller with FW 1.9.2+
-        @SerializedName("favorites_enabled")
-        public Boolean favoritesEnabled = false;
-        public ArrayList<ShellyFavPos> favorites;
-
-        // Motion
-        public ShellyMotionSettings motion;
-        @SerializedName("tamper_sensitivity")
-        public Integer tamperSensitivity;
-        @SerializedName("dark_threshold")
-        public Integer darkThreshold;
-        @SerializedName("twilight_threshold")
-        public Integer twilightThreshold;
-
-        @SerializedName("sleep_time") // Shelly Motion
-        public Integer sleepTime;
-    }
-
-    public static class ShellySettingsAttributes {
-        @SerializedName("device_type")
-        public String deviceType; // Device model identifier
-        @SerializedName("device_mac")
-        public String deviceMac; // MAC address of the device in hexadecimal
-        @SerializedName("wifi_ap")
-        public String wifiAp; // WiFi access poInteger configuration, see /settings/ap for details
-        @SerializedName("wifi_sta")
-        public String wifiSta; // WiFi client configuration. See /settings/sta for details
-        public String login; // credentials used for HTTP Basic authentication for the REST interface. If
-                             // enabled is true clients must include an Authorization: Basic ... HTTP header with valid
-                             // credentials when performing TP requests.
-        public String name; // unique name of the device.
-        public String fw; // current FW version
-    }
-
-    public static class ShellyActionsStats {
-        public Integer skipped;
-    }
-
-    public static class ShellySettingsStatus {
-        public String name; // FW 1.8: Symbolic Device name is configurable
-
-        @SerializedName("wifi_sta")
-        public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details
-        public ShellyStatusCloud cloud;
-        public ShellyStatusMqtt mqtt;
-
-        public String time;
-        public Integer serial = -1;
-        @SerializedName("has_update")
-        public Boolean hasUpdate;
-        public String mac;
-        public Boolean discoverable; // FW 1.6+
-        @SerializedName("cfg_changed_cnt")
-        public Integer cfgChangedCount; // FW 1.8
-        @SerializedName("actions_stats")
-        public ShellyActionsStats astats;
-
-        public ArrayList<ShellySettingsRelay> relays;
-        public Double voltage; // Shelly 2.5
-
-        public ArrayList<ShellySettingsRoller> rollers;
-        public Integer input; // RGBW2 has no JSON array
-        public ArrayList<ShellyInputState> inputs;
-        public ArrayList<ShellySettingsLight> lights;
-        public ArrayList<ShellyShortLightStatus> dimmers;
-        public ArrayList<ShellySettingsMeter> meters;
-        public ArrayList<ShellySettingsEMeter> emeters;
-
-        // Internal device temp
-        public ShellyStatusSensor.ShellySensorTmp tmp; // Shelly 1PM
-        public Double temperature = SHELLY_API_INVTEMP; // Shelly 2.5
-        public Boolean overtemperature;
-
-        // Shelly Dimmer only
-        public Boolean loaderror;
-        public Boolean overload;
-
-        // Shelly TRV
-        public Boolean calibrated;
-        public ArrayList<ShellyThermnostat> thermostats;
-
-        public ShellySettingsUpdate update;
-        @SerializedName("ram_total")
-        public Long ramTotal;
-        @SerializedName("ram_free")
-        public Long ramFree;
-        @SerializedName("fs_size")
-        public Long fsSize;
-        @SerializedName("fs_free")
-        public Long fsFree;
-        public Long uptime;
-
-        @SerializedName("sleep_time") // Shelly Motion
-        public Integer sleepTime;
-
-        public String json;
-    }
-
-    public static class ShellySettingsInput {
-        @SerializedName("btn_type")
-        public String btnType;
-    }
-
-    public static class ShellyControlRelay {
-        // https://shelly-api-docs.shelly.cloud/#shelly1-1pm-settings-relay-0
-        @SerializedName("is_valid")
-        public Boolean isValid;
-        @SerializedName("has_timer")
-        public Boolean hasTimer; // Whether a timer is currently armed for this channel
-        @SerializedName("timer_remaining")
-        public Integer timerRemaining; // FW 1.6+
-        public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
-
-        public String turn; // Accepted values are on and off. This will turn ON/OFF the respective output
-                            // channel when request is sent .
-        public Integer timer; // A one-shot flip-back timer in seconds.
-    }
-
-    public static class ShellyShortStatusRelay {
-        public String name; // FW 1.8+: Channel could now have a logical name
-        @SerializedName("is_valid")
-        public Boolean isValid;
-        public Boolean ison; // Whether output channel is on or off
-        @SerializedName("has_timer")
-        public Boolean hasTimer; // Whether a timer is currently armed for this channel
-        @SerializedName("timer_remaining")
-        public Integer timerRemaining;
-        public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
-        public Double temperature; // Internal device temperature
-        public Boolean overtemperature; // Device over heated
-    }
-
-    public static class ShellyShortLightStatus {
-        public Boolean ison; // Whether output channel is on or off
-        public String mode; // color or white - valid only for Bulb and RGBW2 even Dimmer returns it also
-        public Integer brightness; // brightness: 0.100%
-
-        @SerializedName("has_timer")
-        public Boolean hasTimer;
-    }
-
-    public static class ShellyStatusRelay {
-        public String name; // FW 1.8: Symbolic channel name is configurable
-
-        @SerializedName("wifi_sta")
-        public ShellySettingsWiFiNetwork wifiSta; // WiFi status
-        public ShellySettingsCoiot coiot; // Firmware 1.6+
-        public Integer serial;
-        public String mac; // MAC
-        public ArrayList<ShellyShortStatusRelay> relays; // relay status
-        public ArrayList<ShellySettingsMeter> meters; // current meter value
-        public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
-
-        @SerializedName("ext_temperature")
-        public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
-        @SerializedName("ext_humidity")
-        public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
-
-        public Double temperature; // device temp acc. on the selected temp unit
-        public ShellyStatusSensor.ShellySensorTmp tmp;
-    }
-
-    public static class ShellyStatusDimmer {
-        @SerializedName("wifi_sta")
-        public ShellySettingsWiFiNetwork wifiSta; // WiFi status
-        public ArrayList<ShellyShortLightStatus> lights; // relay status
-        public ArrayList<ShellySettingsMeter> meters; // current meter value
-
-        public ShellyStatusSensor.ShellySensorTmp tmp;
-        public Boolean overtemperature;
-
-        public Boolean loaderror;
-        public Boolean overload;
-    }
-
-    public static class ShellyControlRoller {
-        public String name; // FW 1.8: Symbolic name is configurable
-
-        @SerializedName("roller_pos")
-        public Integer rollerPos; // number Desired position in percent
-        public Integer duration; // If specified, the motor will move for this period in seconds. If missing, the
-                                 // value of maxtime in /settings/roller/N will be used.
-        public String state; // One of stop, open, close
-        public Double power; // Current power consumption in Watts
-        @SerializedName("is_valid")
-        public Boolean isValid; // If the power meter functions properly
-        @SerializedName("safety_switch")
-        public Boolean safetySwitch; // Whether the safety input is currently triggered
-        public Boolean overtemperature;
-        @SerializedName("stop_reason")
-        public String stopReason; // Last cause for stopping: normal, safety_switch, obstacle
-        @SerializedName("last_direction")
-        public String lastDirection; // Last direction of motion, open or close
-        public Boolean calibrating;
-        public Boolean positioning; // true when calibration was performed
-        @SerializedName("current_pos")
-        public Integer currentPos; // current position 0..100, 100=open
-    }
-
-    public static class ShellyOtaCheckResult {
-        public String status;
-    }
-
-    public static class ShellyApRoaming {
-        public Boolean enabled;
-        public Integer threshold;
-    }
-
-    public static class ShellySensorSleepMode {
-        public Integer period;
-        public String unit;
-    }
-
-    // Shelly TRV
-    public class ShellyThermnostat {
-        public class ShellyThermTargetTemp {
-            public Boolean enabled;
-            public Double value;
-            public String unit;
-        }
-
-        public class ShellyThermTemp {
-            public Double value;
-            public String units;
-            @SerializedName("is_valid")
-            public Boolean isValid;
-        }
-
-        public Double pos;
-        @SerializedName("target_t")
-        public ShellyThermTargetTemp targetTemp;
-        public Boolean schedule;
-        @SerializedName("schedule_profile")
-        public Integer profile;
-        @SerializedName("schedule_profile_names")
-        public String[] profileNames;
-        public ShellyThermTemp tmp;
-        @SerializedName("boost_minutes")
-        public Integer boostMinutes;
-    }
-
-    public static class ShellyStatusSensor {
-        // https://shelly-api-docs.shelly.cloud/#h-amp-t-settings
-        public static class ShellySensorTmp {
-            public Double value; // Temperature in configured unites
-            public String units; // 'C' or 'F'
-            public Double tC; // temperature in deg C
-            public Double tF; // temperature in deg F
-            @SerializedName("is_valid")
-            public Boolean isValid; // whether the internal sensor is operating properly
-        }
-
-        public static class ShellySensorHum {
-            public Double value; // relative humidity in %
-        }
-
-        public static class ShellySensorBat {
-            public Double value; // estimated remaining battery capacity in %
-            public Double voltage; // battery voltage
-        };
-
-        // Door/Window sensor
-        public static class ShellySensorState {
-            @SerializedName("is_valid")
-            public Boolean isValid; // whether the internal sensor is operating properly
-            public String state; // Shelly Door/Window
-
-            // Shelly Motion
-            public Boolean motion;
-            public Boolean vibration;
-            @SerializedName("timestamp")
-            public Long motionTimestamp;
-            @SerializedName("active")
-            public Boolean motionActive;
-        }
-
-        public static class ShellySensorLux {
-            @SerializedName("is_valid")
-            public Boolean isValid; // whether the internal sensor is operating properly
-            public Double value;
-
-            public String illumination;
-        }
-
-        public static class ShellySensorAccel {
-            public Integer tilt; // Tilt in °
-            public Integer vibration; // Whether vibration is detected
-        }
-
-        public static class ShellyMotionSettings {
-            public Integer sensitivity;
-            @SerializedName("blind_time_minutes")
-            public Integer blindTimeMinutes;
-            @SerializedName("pulse_count")
-            public Integer pulseCount;
-            @SerializedName("operating_mode")
-            public Integer operatingMode;
-            public Boolean enabled;
-        }
-
-        public static class ShellyExtTemperature {
-            public static class ShellyShortTemp {
-                public Double tC; // temperature in deg C
-                public Double tF; // temperature in deg F
-            }
-
-            // Shelly 1/1PM have up to 3 sensors
-            // for whatever reasons it's not an array, but 3 independent elements
-            @SerializedName("0")
-            public ShellyShortTemp sensor1;
-            @SerializedName("1")
-            public ShellyShortTemp sensor2;
-            @SerializedName("2")
-            public ShellyShortTemp sensor3;
-        }
-
-        public static class ShellyExtHumidity {
-            public static class ShellyShortHum {
-                public Double hum; // Humidity reading of sensor 0, percent
-            }
-
-            // Shelly 1/1PM have up to 3 sensors
-            // for whatever reasons it's not an array, but 3 independent elements
-            @SerializedName("0")
-            public ShellyShortHum sensor1;
-        }
-
-        public static class ShellyADC {
-            public Double voltage;
-        }
-
-        public ShellySensorTmp tmp;
-        public ShellySensorHum hum;
-        public ShellySensorLux lux;
-        public ShellySensorAccel accel;
-        public ShellySensorBat bat;
-        @SerializedName("sensor")
-        public ShellySensorState sensor;
-        public Boolean smoke; // SHelly Smoke
-        public Boolean flood; // Shelly Flood: true = flood condition detected
-        @SerializedName("rain_sensor")
-        public Boolean rainSensor; // Shelly Flood: true=in rain mode
-
-        public Boolean motion; // Shelly Sense: true=motion detected
-        public Boolean charger; // Shelly Sense, TRV: true=charger connected
-
-        @SerializedName("act_reasons")
-        public List<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device
-
-        @SerializedName("sensor_error")
-        public String sensorError; // 1.5.7: Only displayed in case of error
-
-        // FW 1.7: Shelly Gas
-        @SerializedName("gas_sensor")
-        public ShellyStatusGasSensor gasSensor;
-        @SerializedName("concentration")
-        public ShellyStatusGasConcentration concentration;
-        public ArrayList<ShellyStatusValve> valves;
-
-        // FW 1.7 Button
-        @SerializedName("connect_retries")
-        public Integer connectRetries;
-        public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
-
-        // Shelly UNI FW 1.9+
-        public ArrayList<ShellyADC> adcs;
-
-        // Shelly TRV
-        public Boolean calibrated;
-        public ArrayList<ShellyThermnostat> thermostats;
-    }
-
-    public static class ShellySettingsSmoke {
-        @SerializedName("temperature_units")
-        public String temperatureUnits; // Either 'C' or 'F'
-        @SerializedName("temperature_threshold")
-        public Integer temperatureThreshold; // Temperature delta (in configured degree units) which triggers an update
-        @SerializedName("sleep_mode_period")
-        public Integer sleepModePeriod; // Periodic update period in hours, between 1 and 24
-    }
-
-    // Shelly Gas
-    // "gas_sensor":{"sensor_state":"normal","self_test_state":"not_completed","alarm_state":"none"},
-    // "concentration":{"ppm":0,"is_valid":true},
-    public static class ShellyStatusGasSensor {
-        @SerializedName("sensor_state")
-        public String sensorState;
-        @SerializedName("self_test_state")
-        public String selfTestState;
-        @SerializedName("alarm_state")
-        public String alarmState;
-    }
-
-    public static class ShellyStatusGasConcentration {
-        public Integer ppm;
-        @SerializedName("is_valid")
-        public Boolean isValid;
-    }
-
-    public static class ShellyStatusValve {
-        public String state; // closed/opened/not_connected/failure/closing/opening/checking
-    }
-
-    public static class ShellySettingsLight {
-        public Integer red; // red brightness, 0..255, applies in mode="color"
-        public Integer green; // green brightness, 0..255, applies in mode="color"
-        public Integer blue; // blue brightness, 0..255, applies in mode="color"
-        public Integer white; // white brightness, 0..255, applies in mode="color"
-        public Integer gain; // gain for all channels, 0..100, applies in mode="color"
-        public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
-        public Integer brightness; // brightness, 0..100, applies in mode="white"
-        public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
-                               // Change, 3: Breath,
-                               // 4: Flash, 5: On/Off Gradual, 6: Red/Green Change
-        @SerializedName("default_state")
-        public String defaultState; // one of on, off or last
-        @SerializedName("auto_on")
-        public Double autoOn; // see above
-        @SerializedName("auto_off")
-        public Double autoOff; // see above
-
-        public Integer dcpower; // RGW2:Set to true for 24 V power supply, false for 12 V
-
-        // Shelly Dimmer
-        public String mode;
-        public Boolean ison;
-    }
-
-    public static class ShellySettingsNightMode { // FW1.5.7+
-        public Integer enabled;
-        @SerializedName("start_time")
-        public String startTime;
-        @SerializedName("end_time")
-        public String endTime;
-        public Integer brightness;
-    }
-
-    public static class ShellyStatusLightChannel {
-        public Boolean ison;
-        public Double power;
-        public Boolean overpower;
-        @SerializedName("has_timer")
-        public Boolean hasTimer;
-        @SerializedName("timer_started")
-        public Integer timerStarted;
-        @SerializedName("timer_duration")
-        public Integer timerDuration;
-        @SerializedName("timer_remaining")
-        public Integer timerRemaining;
-
-        public Integer red; // red brightness, 0..255, applies in mode="color"
-        public Integer green; // green brightness, 0..255, applies in mode="color"
-        public Integer blue; // blue brightness, 0..255, applies in mode="color"
-        public Integer white; // white brightness, 0..255, applies in mode="color"
-        public Integer gain; // gain for all channels, 0..100, applies in mode="color"
-        public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
-        public Integer brightness; // brightness, 0..100, applies in mode="white"
-        public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
-                               // Change, 3: Breath,
-    }
-
-    public static class ShellyStatusLight {
-        public Boolean ison; // Whether output channel is on or off
-        public Integer input;
-
-        public ArrayList<ShellyStatusLightChannel> lights;
-        public ArrayList<ShellySettingsMeter> meters;
-    }
-
-    public static class ShellySenseKeyCode {
-        String id; // ID of the stored IR code into Shelly Sense.
-        String name; // Short description or name of the stored IR code.
-    }
-
-    public static class ShellySendKeyList {
-        @SerializedName("key_codes")
-        public ArrayList<ShellySenseKeyCode> keyCodes;
-    }
-
-    /**
-     * Shelly Dimmer returns light[]. However, the structure doesn't match the lights[] of a Bulb/RGBW2.
-     * The tag lights[] will be replaced with dimmers[] so this could be mapped to a different Gson structure.
-     * The function requires that it's only called when the device is a dimmer - on get settings and get status
-     *
-     * @param json Input Json as received by the API
-     * @return Modified Json
-     */
-    public static String fixDimmerJson(String json) {
-        return !json.contains("\"lights\":[") ? json
-                : json.replaceFirst(java.util.regex.Pattern.quote("\"lights\":["), "\"dimmers\":[");
-    }
-
-    /**
-     * Convert Shelly Button events into OH button states
-     *
-     * @param eventType S/SS/SSS or L
-     * @return OH button states
-     */
-    public static String mapButtonEvent(String eventType) {
-        // decode different codings
-        // 0..2: CoAP
-        // S/SS/SSS/L: CoAP for Button and xi3
-        // shortpush/double_shortpush/triple_shortpush/longpush: REST
-        switch (eventType) {
-            case "0":
-                return CommonTriggerEvents.RELEASED;
-            case "1":
-            case SHELLY_BTNEVENT_1SHORTPUSH:
-            case SHELLY_EVENT_SHORTPUSH:
-                return CommonTriggerEvents.SHORT_PRESSED;
-            case SHELLY_BTNEVENT_2SHORTPUSH:
-            case SHELLY_EVENT_DOUBLE_SHORTPUSH:
-                return CommonTriggerEvents.DOUBLE_PRESSED;
-            case SHELLY_BTNEVENT_3SHORTPUSH:
-            case SHELLY_EVENT_TRIPLE_SHORTPUSH:
-                return "TRIPLE_PRESSED";
-            case "2":
-            case SHELLY_BTNEVENT_LONGPUSH:
-            case SHELLY_EVENT_LONGPUSH:
-                return CommonTriggerEvents.LONG_PRESSED;
-            case SHELLY_BTNEVENT_SHORTLONGPUSH:
-            case SHELLY_EVENT_SHORT_LONGTPUSH:
-                return "SHORT_LONG_PRESSED";
-            case SHELLY_BTNEVENT_LONGSHORTPUSH:
-            case SHELLY_EVENT_LONG_SHORTPUSH:
-                return "LONG_SHORT_PRESSED";
-            default:
-                return "";
-        }
-    }
-}
index d900ba557fa47a6113c25bc557dd611c2762d754..1f26e50655cbcc24fd87e99bbc3d11b4d8a6d810 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.shelly.internal.api;
 
 import static org.eclipse.jetty.http.HttpStatus.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -33,6 +33,7 @@ public class ShellyApiResult {
     public String response = "";
     public int httpCode = -1;
     public String httpReason = "";
+    public String authResponse = "";
 
     public ShellyApiResult() {
     }
index 774fc88ed9a45c3a5d2b5bddcc3c043ef4a83c72..2d576a27ecd178b1db8c3685080ba54b44da99b2 100644 (file)
@@ -13,7 +13,8 @@
 package org.openhab.binding.shelly.internal.api;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import java.util.HashMap;
@@ -23,12 +24,12 @@ import java.util.regex.Pattern;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsInput;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsInput;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
 import org.openhab.binding.shelly.internal.util.ShellyVersionDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -63,6 +64,7 @@ public class ShellyDeviceProfile {
     public boolean discoverable = true;
     public boolean auth = false;
     public boolean alwaysOn = true;
+    public boolean isGen2 = false;
 
     public String hwRev = "";
     public String hwBatchId = "";
@@ -93,7 +95,7 @@ public class ShellyDeviceProfile {
     public boolean isHT = false; // true for H&T
     public boolean isDW = false; // true for Door Window sensor
     public boolean isButton = false; // true for a Shelly Button 1
-    public boolean isIX3 = false; // true for a Shelly IX
+    public boolean isIX = false; // true for a Shelly IX
     public boolean isTRV = false; // true for a Shelly TRV
 
     public int minTemp = 0; // Bulb/Duo: Min Light Temp
@@ -202,11 +204,12 @@ public class ShellyDeviceProfile {
         boolean isSmoke = thingType.equals(THING_TYPE_SHELLYSMOKE_STR);
         boolean isGas = thingType.equals(THING_TYPE_SHELLYGAS_STR);
         boolean isUNI = thingType.equals(THING_TYPE_SHELLYUNI_STR);
-        isHT = thingType.equals(THING_TYPE_SHELLYHT_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);
         isMotion = thingType.startsWith(THING_TYPE_SHELLYMOTION_STR);
         isSense = thingType.equals(THING_TYPE_SHELLYSENSE_STR);
-        isIX3 = thingType.equals(THING_TYPE_SHELLYIX3_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);
         isSensor = isHT || isFlood || isDW || isSmoke || isGas || isButton || isUNI || isMotion || isSense || isTRV;
         hasBattery = isHT || isFlood || isDW || isSmoke || isButton || isMotion || isTRV;
@@ -261,7 +264,7 @@ public class ShellyDeviceProfile {
         int idx = i + 1; // group names are 1-based
         if (isRGBW2) {
             return CHANNEL_GROUP_LIGHT_CONTROL;
-        } else if (isIX3) {
+        } else if (isIX) {
             return CHANNEL_GROUP_STATUS + idx;
         } else if (isButton) {
             return CHANNEL_GROUP_STATUS;
@@ -275,7 +278,7 @@ public class ShellyDeviceProfile {
 
     public String getInputSuffix(int i) {
         int idx = i + 1; // channel names are 1-based
-        if (isRGBW2 || isIX3) {
+        if (isRGBW2 || isIX) {
             return ""; // RGBW2 has only 1 channel
         } else if (isRoller || isDimmer) {
             // Roller has 2 relays, but it will be mapped to 1 roller with 2 inputs
@@ -294,7 +297,7 @@ public class ShellyDeviceProfile {
         String btnType = "";
         if (isButton) {
             return true;
-        } else if (isIX3 && settings.inputs != null && idx < settings.inputs.size()) {
+        } else if (isIX && settings.inputs != null && idx < settings.inputs.size()) {
             ShellySettingsInput input = settings.inputs.get(idx);
             btnType = getString(input.btnType);
         } else if (isDimmer) {
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpApi.java
deleted file mode 100644 (file)
index 38cb916..0000000
+++ /dev/null
@@ -1,717 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.api;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.HttpStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySendKeyList;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySenseKeyCode;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
-import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
-import org.openhab.core.library.unit.ImperialUnits;
-import org.openhab.core.library.unit.SIUnits;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * {@link ShellyHttpApi} wraps the Shelly REST API and provides various low level function to access the device api (not
- * cloud api).
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyHttpApi implements ShellyApiInterface {
-    public static final String HTTP_HEADER_AUTH = "Authorization";
-    public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
-    public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
-
-    private final Logger logger = LoggerFactory.getLogger(ShellyHttpApi.class);
-    private final HttpClient httpClient;
-    private ShellyThingConfiguration config = new ShellyThingConfiguration();
-    private String thingName;
-    private final Gson gson = new Gson();
-    private int timeoutErrors = 0;
-    private int timeoutsRecovered = 0;
-
-    private ShellyDeviceProfile profile = new ShellyDeviceProfile();
-
-    public ShellyHttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
-        this.httpClient = httpClient;
-        this.thingName = thingName;
-        setConfig(thingName, config);
-        profile.initFromThingType(thingName);
-    }
-
-    @Override
-    public void setConfig(String thingName, ShellyThingConfiguration config) {
-        this.thingName = thingName;
-        this.config = config;
-    }
-
-    @Override
-    public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
-        return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
-    }
-
-    @Override
-    public String setDebug(boolean enabled) throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class);
-    }
-
-    @Override
-    public String getDebugLog(String id) throws ShellyApiException {
-        return callApi("/debug/" + id, String.class);
-    }
-
-    /**
-     * Initialize the device profile
-     *
-     * @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
-     * @return Initialized ShellyDeviceProfile
-     * @throws ShellyApiException
-     */
-    @Override
-    public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
-        String json = request(SHELLY_URL_SETTINGS);
-        if (json.contains("\"type\":\"SHDM-")) {
-            logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
-            json = fixDimmerJson(json);
-        }
-
-        // Map settings to device profile for Light and Sense
-        profile.initialize(thingType, json);
-
-        // 2nd level initialization
-        profile.thingName = profile.hostname;
-        if (profile.isLight && (profile.numMeters == 0)) {
-            logger.debug("{}: Get number of meters from light status", thingName);
-            ShellyStatusLight status = getLightStatus();
-            profile.numMeters = status.meters != null ? status.meters.size() : 0;
-        }
-        if (profile.isSense) {
-            profile.irCodes = getIRCodeList();
-            logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
-        }
-
-        return profile;
-    }
-
-    @Override
-    public boolean isInitialized() {
-        return profile.initialized;
-    }
-
-    /**
-     * Get generic device settings/status. Json returned from API will be mapped to a Gson object
-     *
-     * @return Device settings/status as ShellySettingsStatus object
-     * @throws ShellyApiException
-     */
-    @Override
-    public ShellySettingsStatus getStatus() throws ShellyApiException {
-        String json = "";
-        try {
-            json = request(SHELLY_URL_STATUS);
-            // Dimmer2 returns invalid json type for loaderror :-(
-            json = getString(json.replace("\"loaderror\":0,", "\"loaderror\":false,"));
-            json = getString(json.replace("\"loaderror\":1,", "\"loaderror\":true,"));
-            ShellySettingsStatus status = fromJson(gson, json, ShellySettingsStatus.class);
-            status.json = json;
-            return status;
-        } catch (JsonSyntaxException e) {
-            throw new ShellyApiException("Unable to parse JSON: " + json, e);
-        }
-    }
-
-    @Override
-    public ShellyStatusRelay getRelayStatus(Integer relayIndex) throws ShellyApiException {
-        return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex.toString(), ShellyStatusRelay.class);
-    }
-
-    @Override
-    public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
-        callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
-                ShellyShortLightStatus.class);
-    }
-
-    @Override
-    public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
-        return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
-                ShellyShortLightStatus.class);
-    }
-
-    @Override
-    public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
-        String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
-        request(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness);
-    }
-
-    @Override
-    public ShellyControlRoller getRollerStatus(int idx) throws ShellyApiException {
-        String uri = SHELLY_URL_CONTROL_ROLLER + "/" + idx + "/pos";
-        return callApi(uri, ShellyControlRoller.class);
-    }
-
-    @Override
-    public void setRollerTurn(int idx, String turnMode) throws ShellyApiException {
-        request(SHELLY_URL_CONTROL_ROLLER + "/" + idx + "?go=" + turnMode);
-    }
-
-    @Override
-    public void setRollerPos(int id, int position) throws ShellyApiException {
-        request(SHELLY_URL_CONTROL_ROLLER + "/" + id + "?go=to_pos&roller_pos=" + position);
-    }
-
-    public void setRollerTimer(int idx, int timer) throws ShellyApiException {
-        request(SHELLY_URL_CONTROL_ROLLER + "/" + idx + "?timer=" + timer);
-    }
-
-    @Override
-    public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
-        return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
-    }
-
-    @Override
-    public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
-        ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
-        if (profile.isSense) {
-            // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
-            status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
-                    : ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(status.tmp.value))
-                            .doubleValue();
-            double f = (double) SIUnits.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT)
-                    .convert(getDouble(status.tmp.value));
-            status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
-        }
-        if ((status.charger == null) && (profile.settings.externalPower != null)) {
-            // SHelly H&T uses external_power, Sense uses charger
-            status.charger = profile.settings.externalPower != 0;
-        }
-        return status;
-    }
-
-    @Override
-    public void setTimer(int index, String timerName, int value) throws ShellyApiException {
-        String type = SHELLY_CLASS_RELAY;
-        if (profile.isRoller) {
-            type = SHELLY_CLASS_ROLLER;
-        } else if (profile.isLight) {
-            type = SHELLY_CLASS_LIGHT;
-        }
-        String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
-        request(uri);
-    }
-
-    @Override
-    public void setSleepTime(int value) throws ShellyApiException {
-        request(SHELLY_URL_SETTINGS + "?sleep_time=" + value);
-    }
-
-    @Override
-    public void setValveTemperature(int valveId, int value) throws ShellyApiException {
-        request("/thermostat/" + valveId + "?target_t_enabled=1&target_t=" + value);
-    }
-
-    @Override
-    public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
-        String uri = "/settings/thermostat/" + valveId + "?target_t_enabled=" + (auto ? "1" : "0");
-        if (auto) {
-            uri = uri + "&target_t=" + getDouble(profile.settings.thermostats.get(0).targetTemp.value);
-        }
-        request(uri); // percentage to open the valve
-    }
-
-    @Override
-    public void setValveProfile(int valveId, int value) throws ShellyApiException {
-        String uri = "/settings/thermostat/" + valveId + "?";
-        request(uri + (value == 0 ? "schedule=0" : "schedule=1&schedule_profile=" + value));
-    }
-
-    @Override
-    public void setValvePosition(int valveId, double value) throws ShellyApiException {
-        request("/thermostat/" + valveId + "?pos=" + value); // percentage to open the valve
-    }
-
-    @Override
-    public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
-        request("/settings/thermostat/" + valveId + "?boost_minutes=" + value);
-    }
-
-    @Override
-    public void startValveBoost(int valveId, int value) throws ShellyApiException {
-        int minutes = value != -1 ? value : getInteger(profile.settings.thermostats.get(0).boostMinutes);
-        request("/thermostat/" + valveId + "?boost_minutes=" + minutes);
-    }
-
-    @Override
-    public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
-        request(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
-    }
-
-    public ShellySettingsLight getLightSettings() throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
-    }
-
-    @Override
-    public ShellyStatusLight getLightStatus() throws ShellyApiException {
-        return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
-    }
-
-    public void setLightSetting(String parm, String value) throws ShellyApiException {
-        request(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
-    }
-
-    @Override
-    public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
-    }
-
-    @Override
-    public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + urlEncode(user) + "&password="
-                + urlEncode(password), ShellySettingsLogin.class);
-    }
-
-    @Override
-    public String getCoIoTDescription() throws ShellyApiException {
-        try {
-            return callApi("/cit/d", String.class);
-        } catch (ShellyApiException e) {
-            if (e.getApiResult().isNotFound()) {
-                return ""; // only supported by FW 1.10+
-            }
-            throw e;
-        }
-    }
-
-    @Override
-    public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
-    }
-
-    @Override
-    public String deviceReboot() throws ShellyApiException {
-        return callApi(SHELLY_URL_RESTART, String.class);
-    }
-
-    @Override
-    public String factoryReset() throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
-    }
-
-    @Override
-    public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
-        return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
-    }
-
-    @Override
-    public String setWiFiRecovery(boolean enable) throws ShellyApiException {
-        return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
-                String.class); // FW 1.10+: Enable auto-restart on WiFi problems
-    }
-
-    @Override
-    public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
-        return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
-    }
-
-    @Override
-    public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
-        return callApi("/sta_cache_reset", String.class);
-    }
-
-    public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
-        return callApi("/ota?" + uri, ShellySettingsUpdate.class);
-    }
-
-    @Override
-    public String setCloud(boolean enabled) throws ShellyApiException {
-        return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class);
-    }
-
-    /**
-     * Change between White and Color Mode
-     *
-     * @param mode
-     * @throws ShellyApiException
-     */
-    @Override
-    public void setLightMode(String mode) throws ShellyApiException {
-        if (!mode.isEmpty() && !profile.mode.equals(mode)) {
-            setLightSetting(SHELLY_API_MODE, mode);
-            profile.mode = mode;
-            profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
-        }
-    }
-
-    /**
-     * Set a single light parameter
-     *
-     * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
-     * @param parm Name of the parameter (see API spec)
-     * @param value The value
-     * @throws ShellyApiException
-     */
-    @Override
-    public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
-        // Bulb, RGW2: /<color mode>/<light id>?parm?value
-        // Dimmer: /light/<light id>?parm=value
-        request(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
-    }
-
-    @Override
-    public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
-        String url = getControlUriPrefix(lightIndex) + "?";
-        int i = 0;
-        for (String key : parameters.keySet()) {
-            if (i > 0) {
-                url = url + "&";
-            }
-            url = url + key + "=" + parameters.get(key);
-            i++;
-        }
-        request(url);
-    }
-
-    /**
-     * Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
-     * symbolic key code, which gets
-     * map into a PRONTO code
-     *
-     * @return Map of key codes
-     * @throws ShellyApiException
-     */
-    public Map<String, String> getIRCodeList() throws ShellyApiException {
-        String result = request(SHELLY_URL_LIST_IR);
-        // take pragmatic approach to make the returned JSon into named arrays for Gson parsing
-        String keyList = substringAfter(result, "[");
-        keyList = substringBeforeLast(keyList, "]");
-        keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
-        keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
-        keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
-        String json = "{\"key_codes\" : [" + keyList + "] }";
-        ShellySendKeyList codes = fromJson(gson, json, ShellySendKeyList.class);
-        Map<String, String> list = new HashMap<>();
-        for (ShellySenseKeyCode key : codes.keyCodes) {
-            if (key != null) {
-                list.put(key.id, key.name);
-            }
-        }
-        return list;
-    }
-
-    /**
-     * Sends a IR key code to the Shelly Sense.
-     *
-     * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
-     *            plain or hex64 format
-     *
-     * @throws ShellyApiException
-     * @throws IllegalArgumentException
-     */
-    @Override
-    public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
-        String type = "";
-        if (profile.irCodes.containsKey(keyCode)) {
-            type = SHELLY_IR_CODET_STORED;
-        } else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
-            type = SHELLY_IR_CODET_PRONTO;
-        } else {
-            type = SHELLY_IR_CODET_PRONTO_HEX;
-        }
-        String url = SHELLY_URL_SEND_IR + "?type=" + type;
-        if (type.equals(SHELLY_IR_CODET_STORED)) {
-            url = url + "&" + "id=" + keyCode;
-        } else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
-            String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
-            url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
-        } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
-            url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
-        }
-        request(url);
-    }
-
-    public void setSenseSetting(String setting, String value) throws ShellyApiException {
-        request(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
-    }
-
-    /**
-     * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
-     * redirected to the binding's servlet and act as a trigger to schedule a status update
-     *
-     * @param ShellyApiException
-     * @throws ShellyApiException
-     */
-    @Override
-    public void setActionURLs() throws ShellyApiException {
-        setRelayEvents();
-        setDimmerEvents();
-        setSensorEventUrls();
-    }
-
-    private void setRelayEvents() throws ShellyApiException {
-        if (profile.settings.relays != null) {
-            int num = profile.isRoller ? profile.numRollers : profile.numRelays;
-            for (int i = 0; i < num; i++) {
-                setEventUrls(i);
-            }
-        }
-    }
-
-    private void setDimmerEvents() throws ShellyApiException {
-        if (profile.settings.dimmers != null) {
-            for (int i = 0; i < profile.settings.dimmers.size(); i++) {
-                setEventUrls(i);
-            }
-        } else if (profile.isLight) {
-            setEventUrls(0);
-        }
-    }
-
-    /**
-     * Set sensor Action URLs
-     *
-     * @throws ShellyApiException
-     */
-    private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
-        if (profile.isSensor) {
-            logger.debug("{}: Set Sensor Reporting URL", thingName);
-            setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
-                    SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
-                    SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
-                    SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
-        }
-    }
-
-    /**
-     * Set/delete Relay/Roller/Dimmer Action URLs
-     *
-     * @param index Device Index (0-based)
-     * @throws ShellyApiException
-     */
-    private void setEventUrls(Integer index) throws ShellyApiException {
-        if (profile.isRoller) {
-            setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
-                    SHELLY_EVENT_ROLLER_STOP);
-        } else if (profile.isDimmer) {
-            // 2 set of URLs
-            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
-                    SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
-            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
-                    SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
-
-            // Relay output
-            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
-        } else if (profile.hasRelays) {
-            // Standard relays: btn_xxx, out_xxx, short/longpush URLs
-            setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
-            setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
-            setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
-        } else if (profile.isLight) {
-            // Duo, Bulb
-            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
-        }
-    }
-
-    private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
-        if (config.localIp.isEmpty()) {
-            throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
-        }
-        for (String eventType : eventTypes) {
-            if (profile.containsEventUrl(eventType)) {
-                // H&T adds the type=xx to report_url itself, so we need to ommit here
-                String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
-                String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
-                String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
-                        + profile.thingName + "/" + eclass + urlParm;
-                String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
-                String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
-                if (!enabled && !profile.settingsJson.contains(testUrl)) {
-                    // Don't set URL to null when the current one doesn't point to this OH
-                    // Don't interfere with a 3rd party App
-                    continue;
-                }
-                if (!profile.settingsJson.contains(testUrl)) {
-                    // Current Action URL is != new URL
-                    logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
-                    request(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
-                }
-            }
-        }
-    }
-
-    private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
-            throws ShellyApiException {
-        for (String eventType : eventTypes) {
-            if (profile.containsEventUrl(eventType)) {
-                String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
-                        + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
-                String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
-                String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
-                if (!enabled && !profile.settingsJson.contains(test)) {
-                    // Don't set URL to null when the current one doesn't point to this OH
-                    // Don't interfere with a 3rd party App
-                    continue;
-                }
-                test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
-                if (!profile.settingsJson.contains(test)) {
-                    // Current Action URL is != new URL
-                    logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
-                    request(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType) + "="
-                            + urlEncode(newUrl));
-                }
-            }
-        }
-    }
-
-    private static String mkEventUrl(String eventType) {
-        return eventType + SHELLY_EVENTURL_SUFFIX;
-    }
-
-    /**
-     * Submit GET request and return response, check for invalid responses
-     *
-     * @param uri: URI (e.g. "/settings")
-     */
-    public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
-        String json = request(uri);
-        return fromJson(gson, json, classOfT);
-    }
-
-    private String request(String uri) throws ShellyApiException {
-        ShellyApiResult apiResult = new ShellyApiResult();
-        int retries = 3;
-        boolean timeout = false;
-        while (retries > 0) {
-            try {
-                apiResult = innerRequest(HttpMethod.GET, uri);
-                if (timeout) {
-                    logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
-                            apiResult.getUrl());
-                    timeoutsRecovered++;
-                }
-                return apiResult.response; // successful
-            } catch (ShellyApiException e) {
-                if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
-                    // Sensor in sleep mode or API exception for non-battery device or retry counter expired
-                    throw e; // non-timeout exception
-                }
-
-                timeout = true;
-                retries--;
-                timeoutErrors++; // count the retries
-                logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
-            }
-        }
-        throw new ShellyApiException("API Timeout or inconsistent result"); // successful
-    }
-
-    private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
-        Request request = null;
-        String url = "http://" + config.deviceIp + uri;
-        ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
-
-        try {
-            request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
-                    TimeUnit.MILLISECONDS);
-
-            if (!config.userId.isEmpty()) {
-                String value = config.userId + ":" + config.password;
-                request.header(HTTP_HEADER_AUTH,
-                        HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
-            }
-            request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
-            logger.trace("{}: HTTP {} for {}", thingName, method, url);
-
-            // Do request and get response
-            ContentResponse contentResponse = request.send();
-            apiResult = new ShellyApiResult(contentResponse);
-            String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
-            logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
-
-            // validate response, API errors are reported as Json
-            if (contentResponse.getStatus() != HttpStatus.OK_200) {
-                throw new ShellyApiException(apiResult);
-            }
-            if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
-                    && !url.contains("/sta_cache_reset")) {
-                throw new ShellyApiException("Unexpected response: " + response);
-            }
-        } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
-            ShellyApiException ex = new ShellyApiException(apiResult, e);
-            if (!ex.isTimeout()) { // will be handled by the caller
-                logger.trace("{}: API call returned exception", thingName, ex);
-            }
-            throw ex;
-        }
-        return apiResult;
-    }
-
-    public String getControlUriPrefix(Integer id) {
-        String uri = "";
-        if (profile.isLight || profile.isDimmer) {
-            if (profile.isDuo || profile.isDimmer) {
-                // Duo + Dimmer
-                uri = SHELLY_URL_CONTROL_LIGHT;
-            } else {
-                // Bulb + RGBW2
-                uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
-            }
-        } else {
-            // Roller, Relay
-            uri = SHELLY_URL_CONTROL_RELEAY;
-        }
-        uri = uri + "/" + id;
-        return uri;
-    }
-
-    @Override
-    public int getTimeoutErrors() {
-        return timeoutErrors;
-    }
-
-    @Override
-    public int getTimeoutsRecovered() {
-        return timeoutsRecovered;
-    }
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api/ShellyHttpClient.java
new file mode 100644 (file)
index 0000000..1bc9a8f
--- /dev/null
@@ -0,0 +1,245 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.SHELLY_API_TIMEOUT_MS;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * {@link ShellyHttpClient} implements basic HTTP access
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class ShellyHttpClient {
+    private final Logger logger = LoggerFactory.getLogger(ShellyHttpClient.class);
+
+    public static final String HTTP_HEADER_AUTH = "Authorization";
+    public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
+    public static final String CONTENT_TYPE_JSON = "application/json; charset=UTF-8";
+    public static final String CONTENT_TYPE_FORM_URLENC = "application/x-www-form-urlencoded";
+
+    protected final HttpClient httpClient;
+    protected ShellyThingConfiguration config = new ShellyThingConfiguration();
+    protected String thingName;
+    protected final Gson gson = new Gson();
+    protected int timeoutErrors = 0;
+    protected int timeoutsRecovered = 0;
+    private ShellyDeviceProfile profile;
+
+    public ShellyHttpClient(String thingName, ShellyThingInterface thing) {
+        this(thingName, thing.getThingConfig(), thing.getHttpClient());
+        this.profile = thing.getProfile();
+        profile.initFromThingType(thingName);
+    }
+
+    public ShellyHttpClient(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
+        profile = new ShellyDeviceProfile();
+        this.thingName = thingName;
+        setConfig(thingName, config);
+        this.httpClient = httpClient;
+    }
+
+    public void initialize() throws ShellyApiException {
+    }
+
+    public void setConfig(String thingName, ShellyThingConfiguration config) {
+        this.thingName = thingName;
+        this.config = config;
+    }
+
+    /**
+     * Submit GET request and return response, check for invalid responses
+     *
+     * @param uri: URI (e.g. "/settings")
+     */
+    public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
+        String json = httpRequest(uri);
+        return fromJson(gson, json, classOfT);
+    }
+
+    public <T> T postApi(String uri, String data, Class<T> classOfT) throws ShellyApiException {
+        String json = httpPost(uri, data);
+        return fromJson(gson, json, classOfT);
+    }
+
+    protected String httpRequest(String uri) throws ShellyApiException {
+        ShellyApiResult apiResult = new ShellyApiResult();
+        int retries = 3;
+        boolean timeout = false;
+        while (retries > 0) {
+            try {
+                apiResult = innerRequest(HttpMethod.GET, uri, "");
+                if (timeout) {
+                    logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
+                            apiResult.getUrl());
+                    timeoutsRecovered++;
+                }
+                return apiResult.response; // successful
+            } catch (ShellyApiException e) {
+                if ((!e.isTimeout() && !apiResult.isHttpServerError()) && !apiResult.isNotFound() || profile.hasBattery
+                        || (retries == 0)) {
+                    // Sensor in sleep mode or API exception for non-battery device or retry counter expired
+                    throw e; // non-timeout exception
+                }
+
+                timeout = true;
+                retries--;
+                timeoutErrors++; // count the retries
+                logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
+            }
+        }
+        throw new ShellyApiException("API Timeout or inconsistent result"); // successful
+    }
+
+    public String httpPost(String uri, String data) throws ShellyApiException {
+        return innerRequest(HttpMethod.POST, uri, data).response;
+    }
+
+    private ShellyApiResult innerRequest(HttpMethod method, String uri, String data) throws ShellyApiException {
+        Request request = null;
+        String url = "http://" + config.deviceIp + uri;
+        ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
+
+        try {
+            request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
+                    TimeUnit.MILLISECONDS);
+
+            if (!config.password.isEmpty() && !getString(data).contains("\"auth\":{")) {
+                String value = config.userId + ":" + config.password;
+                request.header(HTTP_HEADER_AUTH,
+                        HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
+            }
+            fillPostData(request, data);
+            logger.trace("{}: HTTP {} for {} {}", thingName, method, url, data);
+
+            // 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);
+
+            HttpFields headers = contentResponse.getHeaders();
+            String auth = headers.get(HttpHeader.WWW_AUTHENTICATE);
+            if (!getString(auth).isEmpty()) {
+                apiResult.authResponse = auth;
+            }
+
+            // validate response, API errors are reported as Json
+            if (apiResult.httpCode != HttpStatus.OK_200) {
+                throw new ShellyApiException(apiResult);
+            }
+
+            if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
+                    && !url.contains("/sta_cache_reset")) {
+                throw new ShellyApiException("Unexpected response: " + response);
+            }
+        } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
+            ShellyApiException ex = new ShellyApiException(apiResult, e);
+            if (!ex.isTimeout()) { // will be handled by the caller
+                logger.trace("{}: API call returned exception", thingName, ex);
+            }
+            throw ex;
+        }
+        return apiResult;
+    }
+
+    /**
+     * Fill in POST data, set http headers
+     *
+     * @param request HTTP request structure
+     * @param data POST data, might be empty
+     */
+    private void fillPostData(Request request, String data) {
+        boolean json = data.startsWith("{") || data.contains("\": {");
+        String type = json ? CONTENT_TYPE_JSON : CONTENT_TYPE_FORM_URLENC;
+        request.header(HttpHeader.CONTENT_TYPE, type);
+        if (!data.isEmpty()) {
+            StringContentProvider postData;
+            postData = new StringContentProvider(type, data, StandardCharsets.UTF_8);
+            request.content(postData);
+            request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
+        }
+    }
+
+    /**
+     * Format POST body depending on content type (JSON or form encoded)
+     *
+     * @param dataMap Field list
+     * @param json true=JSON format, false=form encoded
+     * @return formatted body
+     */
+    public static String buildPostData(Map<String, String> dataMap, boolean json) {
+        String data = "";
+        for (Map.Entry<String, String> e : dataMap.entrySet()) {
+            data = data + (data.isEmpty() ? "" : json ? ", " : "&");
+            if (!json) {
+                data = data + e.getKey() + "=" + e.getValue();
+            } else {
+                data = data + "\"" + e.getKey() + "\" : \"" + e.getValue() + "\"";
+            }
+        }
+        return json ? "{ " + data + " }" : data;
+    }
+
+    public String getControlUriPrefix(Integer id) {
+        String uri = "";
+        if (profile.isLight || profile.isDimmer) {
+            if (profile.isDuo || profile.isDimmer) {
+                // Duo + Dimmer
+                uri = SHELLY_URL_CONTROL_LIGHT;
+            } else {
+                // Bulb + RGBW2
+                uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
+            }
+        } else {
+            // Roller, Relay
+            uri = SHELLY_URL_CONTROL_RELEAY;
+        }
+        uri = uri + "/" + id;
+        return uri;
+    }
+
+    public int getTimeoutErrors() {
+        return timeoutErrors;
+    }
+
+    public int getTimeoutsRecovered() {
+        return timeoutsRecovered;
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1ApiJsonDTO.java
new file mode 100644 (file)
index 0000000..4cf98d3
--- /dev/null
@@ -0,0 +1,1194 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor.ShellyMotionSettings;
+import org.openhab.core.thing.CommonTriggerEvents;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link Shelly1ApiJsonDTO} is used for the JSon/GSon mapping
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class Shelly1ApiJsonDTO {
+    public static final String SHELLY_NULL_URL = "null";
+    public static final String SHELLY_URL_DEVINFO = "/shelly";
+    public static final String SHELLY_URL_STATUS = "/status";
+    public static final String SHELLY_URL_SETTINGS = "/settings";
+    public static final String SHELLY_URL_SETTINGS_AP = "/settings/ap";
+    public static final String SHELLY_URL_SETTINGS_STA = "/settings/sta";
+    public static final String SHELLY_URL_SETTINGS_LOGIN = "/settings/sta";
+    public static final String SHELLY_URL_SETTINGS_CLOUD = "/settings/cloud";
+    public static final String SHELLY_URL_LIST_IR = "/ir/list";
+    public static final String SHELLY_URL_SEND_IR = "/ir/emit";
+    public static final String SHELLY_URL_RESTART = "/reboot";
+
+    public static final String SHELLY_URL_SETTINGS_RELAY = "/settings/relay";
+    public static final String SHELLY_URL_STATUS_RELEAY = "/status/relay";
+    public static final String SHELLY_URL_CONTROL_RELEAY = "/relay";
+
+    public static final String SHELLY_URL_SETTINGS_EMETER = "/settings/emeter";
+    public static final String SHELLY_URL_STATUS_EMETER = "/emeter";
+    public static final String SHELLY_URL_DATA_EMETER = "/emeter/{0}/em_data.csv";
+
+    public static final String SHELLY_URL_CONTROL_ROLLER = "/roller";
+    public static final String SHELLY_URL_SETTINGS_ROLLER = "/settings/roller";
+
+    public static final String SHELLY_URL_SETTINGS_LIGHT = "/settings/light";
+    public static final String SHELLY_URL_STATUS_LIGHT = "/light";
+    public static final String SHELLY_URL_CONTROL_LIGHT = "/light";
+
+    public static final String SHELLY_URL_SETTINGS_DIMMER = "/settings/light";
+
+    // Wakeup reasons
+    public static final String SHELLY_WAKEUPT_SENSOR = "SENSOR"; // new sensordata
+    public static final String SHELLY_WAKEUPT_PERIODIC = "PERIODIC"; // periodic wakeup
+    public static final String SHELLY_WAKEUPT_BUTTON = "BUTTON"; // button pressed
+    public static final String SHELLY_WAKEUPT_POWERON = "POWERON"; // device powered up
+    public static final String SHELLY_WAKEUPT_EXT_POWER = "EXT_POWER"; // charger connected
+    public static final String SHELLY_WAKEUPT_UNKNOWN = "UNKNOWN"; // other event
+
+    //
+    // Action URLs according to the device type
+    //
+    public static final String SHELLY_EVENTURL_SUFFIX = "_url";
+
+    // Relay
+    public static final String SHELLY_EVENT_BTN_ON = "btn_on";
+    public static final String SHELLY_EVENT_BTN_OFF = "btn_off";
+    public static final String SHELLY_EVENT_OUT_ON = "out_on";
+    public static final String SHELLY_EVENT_OUT_OFF = "out_off";
+    public static final String SHELLY_EVENT_SHORTPUSH = "shortpush";
+    public static final String SHELLY_EVENT_LONGPUSH = "longpush";
+    // Button
+    public static final String SHELLY_EVENT_DOUBLE_SHORTPUSH = "double_shortpush";
+    public static final String SHELLY_EVENT_TRIPLE_SHORTPUSH = "triple_shortpush";
+    public static final String SHELLY_EVENT_SHORT_LONGTPUSH = "shortpush_longpush";
+    public static final String SHELLY_EVENT_LONG_SHORTPUSH = "longpush_shortpush";
+
+    // Dimmer
+    public static final String SHELLY_EVENT_BTN1_ON = "btn1_on";
+    public static final String SHELLY_EVENT_BTN1_OFF = "btn1_off";
+    public static final String SHELLY_EVENT_BTN2_ON = "btn2_on";
+    public static final String SHELLY_EVENT_BTN2_OFF = "btn2_off";
+    public static final String SHELLY_EVENT_SHORTPUSH1 = "btn1_shortpush";
+    public static final String SHELLY_EVENT_LONGPUSH1 = "btn1_longpush";
+    public static final String SHELLY_EVENT_SHORTPUSH2 = "btn2_shortpush";
+    public static final String SHELLY_EVENT_LONGPUSH2 = "btn2_longpush";
+
+    // Roller
+    public static final String SHELLY_EVENT_ROLLER_OPEN = "roller_open";
+    public static final String SHELLY_EVENT_ROLLER_CLOSE = "roller_close";
+    public static final String SHELLY_EVENT_ROLLER_STOP = "roller_stop";
+    public static final String SHELLY_EVENT_ROLLER_CALIB = "roller_calibrating";
+
+    // Roller states
+    public static final String SHELLY_RSTATE_OPEN = "open";
+    public static final String SHELLY_RSTATE_STOP = "stop";
+    public static final String SHELLY_RSTATE_CLOSE = "close";
+
+    // Sensors
+    public static final String SHELLY_EVENT_SENSORREPORT = "report";
+    public static final String SHELLY_EVENT_DARK = "dark";
+    public static final String SHELLY_EVENT_TWILIGHT = "twilight";
+    public static final String SHELLY_EVENT_BRIGHT = "bright";
+    public static final String SHELLY_EVENT_FLOOD_DETECTED = "flood_detected";
+    public static final String SHELLY_EVENT_FLOOD_GONE = "flood_gone";
+    public static final String SHELLY_EVENT_VIBRATION = "vibration"; // DW 1.6.5+
+    public static final String SHELLY_EVENT_OPEN = "open"; // DW 1.6.5+
+    public static final String SHELLY_EVENT_CLOSE = "close"; // DW 1.6.5+
+    public static final String SHELLY_EVENT_TEMP_OVER = "temp_over"; // FW 1.7
+    public static final String SHELLY_EVENT_TEMP_UNDER = "temp_under"; // FW 1.7
+
+    // Gas
+    public static final String SHELLY_EVENT_ALARM_MILD = "alarm_mild"; // DW 1.7+
+    public static final String SHELLY_EVENT_ALARM_HEAVY = "alarm_heavy"; // DW 1.7+
+    public static final String SHELLY_EVENT_ALARM_OFF = "alarm_off"; // DW 1.7+
+
+    //
+    // API values
+    //
+    public static final double SHELLY_API_INVTEMP = -999.0;
+
+    public static final String SHELLY_BTNT_MOMENTARY = "momentary";
+    public static final String SHELLY_BTNT_MOM_ON_RELEASE = "momentary_on_release";
+    public static final String SHELLY_BTNT_ONE_BUTTON = "one_button";
+    public static final String SHELLY_BTNT_TWO_BUTTON = "dual_button";
+    public static final String SHELLY_BTNT_TOGGLE = "toggle";
+    public static final String SHELLY_BTNT_EDGE = "edge";
+    public static final String SHELLY_BTNT_DETACHED = "detached";
+
+    public static final String SHELLY_STATE_LAST = "last";
+    public static final String SHELLY_STATE_STOP = "stop";
+
+    public static final String SHELLY_INP_MODE_OPENCLOSE = "openclose";
+    public static final String SHELLY_INP_MODE_ONEBUTTON = "onebutton";
+
+    public static final String SHELLY_OBSTMODE_DISABLED = "disabled";
+    public static final String SHELLY_SAFETYM_WHILEOPENING = "while_opening";
+
+    public static final String SHELLY_ALWD_TRIGGER_NONE = "none";
+    public static final String SHELLY_ALWD_ROLLER_TURN_OPEN = "open";
+    public static final String SHELLY_ALWD_ROLLER_TURN_CLOSE = "close";
+    public static final String SHELLY_ALWD_ROLLER_TURN_STOP = "stop";
+
+    // API Error Codes
+    public static final String SHELLY_APIERR_UNAUTHORIZED = "Unauthorized";
+    public static final String SHELLY_APIERR_TIMEOUT = "Timeout";
+    public static final String SHELLY_APIERR_NOT_CALIBRATED = "Not calibrated!";
+
+    // API device types / properties
+    public static final String SHELLY_CLASS_RELAY = "relay"; // Relay: relay mode
+    public static final String SHELLY_CLASS_ROLLER = "roller"; // Relay: roller mode
+    public static final String SHELLY_CLASS_LIGHT = "light"; // Bulb: color mode
+    public static final String SHELLY_CLASS_EMETER = "emeter"; // EM/EM3: emeter
+
+    public static final String SHELLY_API_ON = "on";
+    public static final String SHELLY_API_OFF = "off";
+    public static final String SHELLY_API_TRUE = "true";
+    public static final String SHELLY_API_FALSE = "false";
+
+    public static final String SHELLY_API_MODE = "mode";
+    public static final String SHELLY_MODE_RELAY = "relay"; // Relay: relay mode
+    public static final String SHELLY_MODE_ROLLER = "roller"; // Relay: roller mode
+    public static final String SHELLY_MODE_COLOR = "color"; // Bulb/RGBW2: color mode
+    public static final String SHELLY_MODE_WHITE = "white"; // Bulb/RGBW2: white mode
+
+    public static final String SHELLY_LED_STATUS_DISABLE = "led_status_disable";
+    public static final String SHELLY_LED_POWER_DISABLE = "led_power_disable";
+
+    public static final String SHELLY_API_STOPR_NORMAL = "normal";
+    public static final String SHELLY_API_STOPR_SAFETYSW = "safety_switch";
+    public static final String SHELLY_API_STOPR_OBSTACLE = "obstacle";
+    public static final String SHELLY_API_STOPR_OVERPOWER = "overpower";
+
+    public static final String SHELLY_TIMER_AUTOON = "auto_on";
+    public static final String SHELLY_TIMER_AUTOOFF = "auto_off";
+    public static final String SHELLY_TIMER_ACTIVE = "has_timer";
+
+    public static final String SHELLY_LIGHT_TURN = "turn";
+    public static final String SHELLY_LIGHT_DEFSTATE = "def_state";
+    public static final String SHELLY_LIGHTTIMER = "timer";
+
+    public static final String SHELLY_COLOR_RED = "red";
+    public static final String SHELLY_COLOR_BLUE = "blue";
+    public static final String SHELLY_COLOR_GREEN = "green";
+    public static final String SHELLY_COLOR_YELLOW = "yellow";
+    public static final String SHELLY_COLOR_WHITE = "white";
+    public static final String SHELLY_COLOR_GAIN = "gain";
+    public static final String SHELLY_COLOR_BRIGHTNESS = "brightness";
+    public static final String SHELLY_COLOR_TEMP = "temp";
+    public static final String SHELLY_COLOR_EFFECT = "effect";
+
+    public static final int SHELLY_MIN_ROLLER_POS = 0;
+    public static final int SHELLY_MAX_ROLLER_POS = 100;
+    public static final int SHELLY_MIN_BRIGHTNESS = 0;
+    public static final int SHELLY_MAX_BRIGHTNESS = 100;
+    public static final int SHELLY_MIN_GAIN = 0;
+    public static final int SHELLY_MAX_GAIN = 100;
+    public static final int SHELLY_MIN_COLOR = 0;
+    public static final int SHELLY_MAX_COLOR = 255;
+    public static final int SHELLY_DIM_STEPSIZE = 10;
+
+    // color temperature: 3000 = warm, 4750 = white, 6565 = cold; gain: 0..100
+    public static final int MIN_COLOR_TEMP_BULB = 3000;
+    public static final int MAX_COLOR_TEMP_BULB = 6500;
+    public static final int MIN_COLOR_TEMP_DUO = 2700;
+    public static final int MAX_COLOR_TEMP_DUO = 6500;
+    public static final int COLOR_TEMP_RANGE_BULB = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
+    public static final int COLOR_TEMP_RANGE_DUO = MAX_COLOR_TEMP_DUO - MIN_COLOR_TEMP_DUO;
+    public static final double MIN_BRIGHTNESS = 0.0;
+    public static final double MAX_BRIGHTNESS = 100.0;
+    public static final double SATURATION_FACTOR = 2.55;
+    public static final double GAIN_FACTOR = SHELLY_MAX_GAIN / 100;
+    public static final double BRIGHTNESS_FACTOR = SHELLY_MAX_BRIGHTNESS / 100;
+
+    // Door/Window
+    public static final String SHELLY_API_ILLUM_DARK = "dark";
+    public static final String SHELLY_API_ILLUM_TWILIGHT = "twilight";
+    public static final String SHELLY_API_ILLUM_BRIGHT = "bright";
+    public static final String SHELLY_API_DWSTATE_OPEN = "open";
+    public static final String SHELLY_API_DWSTATE_CLOSE = "close";
+
+    // Shelly Sense
+    public static final String SHELLY_IR_CODET_STORED = "stored";
+    public static final String SHELLY_IR_CODET_PRONTO = "pronto";
+    public static final String SHELLY_IR_CODET_PRONTO_HEX = "pronto_hex";
+
+    // Bulb/Duo/RGBW2
+    public static final int SHELLY_MIN_EFFECT = 0;
+    public static final int SHELLY_MAX_EFFECT = 6;
+
+    // Button
+    public static final String SHELLY_BTNEVENT_1SHORTPUSH = "S";
+    public static final String SHELLY_BTNEVENT_2SHORTPUSH = "SS";
+    public static final String SHELLY_BTNEVENT_3SHORTPUSH = "SSS";
+    public static final String SHELLY_BTNEVENT_LONGPUSH = "L";
+    public static final String SHELLY_BTNEVENT_SHORTLONGPUSH = "SL";
+    public static final String SHELLY_BTNEVENT_LONGSHORTPUSH = "LS";
+
+    public static final String SHELLY_TEMP_CELSIUS = "C";
+    public static final String SHELLY_TEMP_FAHRENHEIT = "F";
+
+    // Motion
+    public static final int SHELLY_MOTION_SLEEPTIME_OFFSET = 3; // we need to substract and offset
+
+    // TRV
+    public static final int SHELLY_TRV_MIN_TEMP = 5; // < 5: means: lowest (valve fully closed)
+    public static final int SHELLY_TRV_MAX_TEMP = 30; // > 30: means: highest (valve fully open)
+
+    public static final String SHELLY_TRV_MODE_MANUAL = "manual";
+    public static final String SHELLY_TRV_MODE_AUTO = "automatic";
+
+    // CoIoT Multicast setting
+    public static final String SHELLY_COIOT_MCAST = "mcast";
+
+    public static class ShellySettingsDevice {
+        public String type;
+        public String mac;
+        public String hostname;
+        public String fw;
+        public Boolean auth;
+        public Integer gen;
+
+        @SerializedName("coiot") // Shelly Motion Multicast Endpoint
+        public String coiot;
+        public Integer longid;
+
+        @SerializedName("num_outputs")
+        public Integer numOutputs;
+        @SerializedName("num_meters")
+        public Integer numMeters;
+        @SerializedName("num_emeters")
+        public Integer numEMeters;
+        @SerializedName("num_rollers")
+        public Integer numRollers;
+    }
+
+    public static class ShellySettingsWiFiAp {
+        public Boolean enabled;
+        public String ssid;
+        public String key;
+    }
+
+    public static class ShellySettingsWiFiNetwork {
+        public Boolean enabled;
+        public String ssid;
+        public Integer rssi;
+
+        @SerializedName("ipv4_method")
+        public String ipv4Method;
+        public String ip;
+        public String gw;
+        public String mask;
+        public String dns;
+    }
+
+    public static class ShellySettingsMqtt {
+        public Boolean enable;
+        public String server;
+        public String user;
+        @SerializedName("reconnect_timeout_max")
+        public Double reconnectTimeoutMax;
+        @SerializedName("reconnect_timeout_min")
+        public Double reconnectTimeoutMin;
+        @SerializedName("clean_session")
+        public Boolean cleanSession;
+        @SerializedName("keep_alive")
+        public Integer keepAlive;
+        @SerializedName("will_topic")
+        public String willTopic;
+        @SerializedName("will_message")
+        public String willMessage;
+        @SerializedName("max_qos")
+        public Integer maxQOS;
+        public Boolean retain;
+        @SerializedName("update_period")
+        public Integer updatePeriod;
+    }
+
+    public static class ShellySettingsCoiot { // FW 1.6+
+        @SerializedName("update_period")
+        public Integer updatePeriod;
+        public Boolean enabled; // Motion 1.0.7: Coap can be disabled
+        public String peer; // if set the device uses singlecast CoAP, mcast=set back to Multicast
+    }
+
+    public static class ShellyStatusMqtt {
+        public Boolean connected;
+    }
+
+    public static class ShellySettingsSntp {
+        public String server;
+        public Boolean enabled;
+    }
+
+    public static class ShellySettingsLogin {
+        public Boolean enabled;
+        public Boolean unprotected;
+        public String username;
+        public String password;
+    }
+
+    public static class ShellySettingsBuildInfo {
+        @SerializedName("build_id")
+        public String buildId;
+        @SerializedName("build_timestamp")
+        public String buildTimestamp;
+        @SerializedName("build_version")
+        public String buildVersion;
+    }
+
+    public static class ShellyStatusCloud {
+        public Boolean enabled;
+        public Boolean connected;
+    }
+
+    public static class ShellySettingsHwInfo {
+        @SerializedName("hw_revision")
+        public String hwRevision;
+        @SerializedName("batch_id")
+        public Integer batchId;
+    }
+
+    public static class ShellySettingsScheduleRules {
+    }
+
+    public static class ShellySettingsRelay {
+        public String name;
+        @SerializedName("default_state")
+        public String defaultState; // Accepted values: off, on, last, switch
+        @SerializedName("btn_type")
+        public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+        @SerializedName("btn1_type") // Shelly 1L
+        public String btnType1;
+        @SerializedName("btn2_type") // Shelly 1L
+        public String btnType2;
+        @SerializedName("has_timer")
+        public Boolean hasTimer; // Whether a timer is currently armed for this channel
+        @SerializedName("auto_on")
+        public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
+        @SerializedName("auto_off")
+        public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
+        @SerializedName("btn_on_url")
+        public String btnOnUrl; // input is activated
+        @SerializedName("btnOffUrl")
+        public String btnOffUrl; // input is deactivated
+        @SerializedName("out_on_url")
+        public String outOnUrl; // output is activated
+        @SerializedName("out_off_url")
+        public String outOffUrl; // output is deactivated
+        @SerializedName("roller_open_url")
+        public String rollerOpenUrl; // to access when roller reaches open position
+        @SerializedName("roller_close_url")
+        public String rollerCloseUrl; // to access when roller reaches close position
+        @SerializedName("roller_stop_url")
+        public String rollerStopUrl; // to access when roller stopped
+        @SerializedName("longpush_url")
+        public String pushLongUrl; // to access when roller stopped
+        @SerializedName("shortpush_url")
+        public String pushShortUrl; // to access when roller stopped
+
+        // Status information
+        public Boolean ison;
+        public Boolean overpower;
+        @SerializedName("is_valid")
+        public Boolean isValid;
+        @SerializedName("ext_temperature")
+        public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
+        @SerializedName("ext_humidity")
+        public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
+    }
+
+    public static class ShellySettingsDimmer {
+        public String name; // unique name of the device
+        public Boolean ison; // true: output is ON
+        @SerializedName("default_state")
+        public String defaultState; // Accepted values: off, on, last, switch
+        @SerializedName("auto_on")
+        public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
+        @SerializedName("auto_off")
+        public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
+        @SerializedName("btn1_on_url")
+        public String btn1OnUrl; // URL to access when SW input is activated
+        @SerializedName("btn1_off_url")
+        public String btn1OffUrl; // URL to access when SW input is deactivated
+        @SerializedName("btn2_on_url")
+        public String btn2OnUrl; // URL to access when SW input is activated
+        @SerializedName("btn2_off_url")
+        public String btn2OoffUrl; // URL to access when SW input is deactivated
+        @SerializedName("out_on_url")
+        public String outOnUrl; // URL to access when output is activated
+        @SerializedName("out_off_url")
+        public String outOffUrl; // URL to access when output is deactivated
+        @SerializedName("longpush_url")
+        public String pushLongUrl; // long push button event
+        @SerializedName("shortpush_url")
+        public String pushShortUrl; // short push button event
+        @SerializedName("btn_type")
+        public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+        @SerializedName("btn1_type")
+        public String btnType1; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+        @SerializedName("btn2_type")
+        public String btnType2; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+        @SerializedName("swap_inputs")
+        public Integer swapInputs; // 0=no
+    }
+
+    public static class ShellySettingsRoller {
+        public Double maxtime;
+        @SerializedName("maxtime_open")
+        public Double maxtimeOpen;
+        @SerializedName("maxtime_close")
+        public Double maxtimeClose;
+        @SerializedName("default_state")
+        public String defaultState; // see SHELLY_STATE_xxx
+        public Boolean swap;
+        @SerializedName("swap_inputs")
+        public Boolean swapInputs;
+        @SerializedName("input_mode")
+        public String inputMode; // see SHELLY_INP_MODE_OPENCLOSE
+        @SerializedName("button_type")
+        public String buttonType; // // see SHELLY_BTNT_xxx
+        @SerializedName("btn_Reverse")
+        public Integer btnReverse;
+        public String state;
+        public Double power;
+        @SerializedName("is_valid")
+        public Boolean isValid;
+        @SerializedName("safety_switch")
+        public Boolean safetySwitch;
+        @SerializedName("obstacle_mode")
+        public String obstaclMode; // SHELLY_OBSTMODE_
+        @SerializedName("obstacle_action")
+        public String obstacleAction; // see SHELLY_STATE_xxx
+        @SerializedName("obstacle_power")
+        public Integer obstaclePower;
+        @SerializedName("obstacle_delay")
+        public Integer obstacleDelay;
+        @SerializedName("safety_mode")
+        public String safetyMode; // see SHELLY_SAFETYM_xxx
+        @SerializedName("safety_action")
+        public String safetyAction; // see SHELLY_STATE_xxx
+        @SerializedName("safety_allowed_on_trigger")
+        public String safetyAllowedOnTrigger; // see SHELLY_ALWD_TRIGGER_xxx
+        @SerializedName("off_power")
+        public Integer offPower;
+        public Boolean positioning;
+    }
+
+    public static class ShellySettingsRgbwLight {
+        public String name;
+        public Boolean ison; // true: output is ON
+        public Integer brightness;
+        public Integer temp;
+        public Integer transition;
+        @SerializedName("default_state")
+        public String defaultState;
+        @SerializedName("auto_on")
+        public Double autoOn; // Automatic flip back timer, seconds. Will engage after turning Shelly1 OFF.
+        @SerializedName("auto_off")
+        public Double autoOff; // Automatic flip back timer, seconds. Will engage after turning Shelly1 ON.
+        public Boolean schedule;
+        @SerializedName("btn_type")
+        public String btnType; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+        @SerializedName("btn_reverse")
+        public Integer btnReverse; // Accepted values: momentary, toggle, edge, detached - // see SHELLY_BTNT_xxx
+        @SerializedName("out_on_url")
+        public String outOnUrl; // output is activated
+        @SerializedName("out_off_url")
+        public String outOffUrl; // output is deactivated
+    }
+
+    public static class ShellyFavPos { // FW 1.9.2+ in roller mode
+        public String name;
+        public Integer pos;
+    }
+
+    public static class ShellyInputState {
+        public Integer input;
+
+        // Shelly Button
+        public String event;
+        @SerializedName("event_cnt")
+        public Integer eventCount;
+    }
+
+    public static class ShellySettingsMeter {
+        @SerializedName("is_valid")
+        public Boolean isValid;
+        public Double power;
+        public Double[] counters = { 0.0, 0.0, 0.0 };
+        public Double total;
+        public Long timestamp;
+    }
+
+    public static class ShellySettingsEMeter { // ShellyEM meter
+        @SerializedName("is_valid")
+        public Boolean isValid; // Whether the associated meter is functioning properly
+        public Double power; // Instantaneous power, Watts
+        public Double reactive; // Instantaneous reactive power, Watts
+        public Double voltage; // RMS voltage, Volts
+        public Double total; // Total consumed energy, Wh
+        @SerializedName("total_returned")
+        public Double totalReturned; // Total returned energy, Wh
+
+        public Double pf; // 3EM
+        public Double current; // 3EM
+    }
+
+    public static class ShellySettingsUpdate {
+        public String status;
+        @SerializedName("has_update")
+        public Boolean hasUpdate;
+        @SerializedName("new_version")
+        public String newVersion;
+        @SerializedName("old_version")
+        public String oldVersion;
+        @SerializedName("beta_version")
+        public String betaVersion;
+    }
+
+    public static class ShellySettingsGlobal {
+        // https://shelly-api-docs.shelly.cloud/#shelly1pm-settings
+        public ShellySettingsDevice device;
+        @SerializedName("wifi_ap")
+        public ShellySettingsWiFiAp wifiAp;
+        @SerializedName("wifi_sta")
+        public ShellySettingsWiFiNetwork wifiSta;
+        @SerializedName("wifi_sta1")
+        public ShellySettingsWiFiNetwork wifiSta1;
+        @SerializedName("wifirecovery_reboot_enabled")
+        public Boolean wifiRecoveryReboot; // FW 1.10+
+        @SerializedName("ap_roaming")
+        public ShellyApRoaming apRoaming; // FW 1.10+
+
+        public ShellySettingsMqtt mqtt; // not used for now
+        public ShellySettingsSntp sntp; // not used for now
+        public ShellySettingsCoiot coiot; // Firmware 1.6+
+        public ShellySettingsLogin login;
+        @SerializedName("pin_code")
+        public String pinCode;
+        @SerializedName("coiot_execute_enable")
+        public Boolean coiotExecuteEnable;
+        public String name;
+        public Boolean discoverable; // FW 1.6+
+        public String fw;
+        @SerializedName("build_info")
+        public ShellySettingsBuildInfo buildInfo;
+        public ShellyStatusCloud cloud;
+        @SerializedName("sleep_mode")
+        public ShellySensorSleepMode sleepMode; // FW 1.6
+        @SerializedName("external_power")
+        public Integer externalPower; // H&T FW 1.6, seems to be the same like charger for the Sense
+        @SerializedName("debug_enable") // FW 1.10+
+        public Boolean debugEnable;
+
+        public String timezone;
+        public Double lat;
+        public Double lng;
+        public Boolean tzautodetect;
+        public String time;
+
+        public ShellySettingsHwInfo hwinfo;
+        public String mode;
+        @SerializedName("max_power")
+        public Double maxPower;
+        public Boolean calibrated;
+
+        public ArrayList<ShellySettingsRelay> relays;
+        public ArrayList<ShellySettingsRoller> rollers;
+        public ArrayList<ShellySettingsDimmer> dimmers;
+        public ArrayList<ShellySettingsRgbwLight> lights;
+        public ArrayList<ShellySettingsEMeter> emeters;
+        public ArrayList<ShellySettingsInput> inputs; // ix3
+        public ArrayList<ShellyThermnostat> thermostats; // TRV
+
+        public Double voltage; // AC voltage for Shelly 2.5
+        @SerializedName("supply_voltage")
+        public Long supplyVoltage; // Shelly 1PM/1L: 0=110V, 1=220V
+
+        @SerializedName("temperature_units")
+        public String temperatureUnits = "C"; // Either'C'or'F'
+
+        @SerializedName("led_status_disable")
+        public Boolean ledStatusDisable; // PlugS only Disable LED indication for network
+                                         // status
+        @SerializedName("led_power_disable")
+        public Boolean ledPowerDisable; // PlugS only Disable LED indication for network
+                                        // status
+        @SerializedName("light_sensor")
+        public String lightSensor; // Sense: sensor type
+        @SerializedName("rain_sensor")
+        public Boolean rainSensor; // Flood: true=in rain mode
+
+        // FW 1.5.7: Door Window
+        @SerializedName("dark_treshold")
+        public Integer darkTreshold; // Illumination definition for "dark" in lux
+        @SerializedName("twilight_treshold")
+        public Integer twiLightTreshold; // Illumination definition for "twilight" in lux
+        @SerializedName("dark_url")
+        public String darkUrl; // URL to report to when luminance <= dark_threshold
+        @SerializedName("twilight_url")
+        public String twiLightUrl; // URL reports when luminance > dark_threshold AND luminance <=
+        @SerializedName("close_url")
+        public String closeUrl; // URL reports when DW contact is closed FW 1.6.5+
+        @SerializedName("vibration_url")
+        public String vibrationUrl; // URL reports when DW detects vibration FW 1.6.5+
+
+        // Gas FW 1.7
+        @SerializedName("set_volume")
+        public Integer volume; // Speaker volume for alarm
+        @SerializedName("alarm_off_url")
+        public String alarmOffUrl; // URL reports when alarm went off
+        @SerializedName("alarm_mild_url")
+        public String alarmMidUrl; // URL reports middle alarm
+        @SerializedName("alarm_heavy_url")
+        public String alarmHeavyfUrl; // URL reports heavy alarm
+
+        // Roller with FW 1.9.2+
+        @SerializedName("favorites_enabled")
+        public Boolean favoritesEnabled = false;
+        public ArrayList<ShellyFavPos> favorites;
+
+        // Motion
+        public ShellyMotionSettings motion;
+        @SerializedName("tamper_sensitivity")
+        public Integer tamperSensitivity;
+        @SerializedName("dark_threshold")
+        public Integer darkThreshold;
+        @SerializedName("twilight_threshold")
+        public Integer twilightThreshold;
+
+        @SerializedName("sleep_time") // Shelly Motion
+        public Integer sleepTime;
+    }
+
+    public static class ShellySettingsAttributes {
+        @SerializedName("device_type")
+        public String deviceType; // Device model identifier
+        @SerializedName("device_mac")
+        public String deviceMac; // MAC address of the device in hexadecimal
+        @SerializedName("wifi_ap")
+        public String wifiAp; // WiFi access poInteger configuration, see /settings/ap for details
+        @SerializedName("wifi_sta")
+        public String wifiSta; // WiFi client configuration. See /settings/sta for details
+        public String login; // credentials used for HTTP Basic authentication for the REST interface. If
+                             // enabled is true clients must include an Authorization: Basic ... HTTP header with valid
+                             // credentials when performing TP requests.
+        public String name; // unique name of the device.
+        public String fw; // current FW version
+    }
+
+    public static class ShellyActionsStats {
+        public Integer skipped;
+    }
+
+    public static class ShellySettingsStatus {
+        public String name; // FW 1.8: Symbolic Device name is configurable
+
+        @SerializedName("wifi_sta")
+        public ShellySettingsWiFiNetwork wifiSta; // WiFi client configuration. See /settings/sta for details
+        public ShellyStatusCloud cloud;
+        public ShellyStatusMqtt mqtt;
+
+        public String time;
+        public Integer serial = -1;
+        @SerializedName("has_update")
+        public Boolean hasUpdate;
+        public String mac;
+        public Boolean discoverable; // FW 1.6+
+        @SerializedName("cfg_changed_cnt")
+        public Integer cfgChangedCount; // FW 1.8
+        @SerializedName("actions_stats")
+        public ShellyActionsStats astats;
+
+        public ArrayList<ShellySettingsRelay> relays;
+        public Double voltage; // Shelly 2.5
+
+        public ArrayList<ShellySettingsRoller> rollers;
+        public Integer input; // RGBW2 has no JSON array
+        public ArrayList<ShellyInputState> inputs;
+        public ArrayList<ShellySettingsLight> lights;
+        public ArrayList<ShellyShortLightStatus> dimmers;
+        public ArrayList<ShellySettingsMeter> meters;
+        public ArrayList<ShellySettingsEMeter> emeters;
+
+        // Internal device temp
+        public ShellySensorTmp tmp; // Shelly 1PM
+        public Double temperature = SHELLY_API_INVTEMP; // Shelly 2.5
+        public Boolean overtemperature;
+
+        // Shelly Dimmer only
+        public Boolean loaderror;
+        public Boolean overload;
+
+        // Shelly TRV
+        public Boolean calibrated;
+        public ArrayList<ShellyThermnostat> thermostats;
+
+        public ShellySettingsUpdate update;
+        @SerializedName("ram_total")
+        public Long ramTotal;
+        @SerializedName("ram_free")
+        public Long ramFree;
+        @SerializedName("fs_size")
+        public Long fsSize;
+        @SerializedName("fs_free")
+        public Long fsFree;
+        public Long uptime;
+
+        @SerializedName("sleep_time") // Shelly Motion
+        public Integer sleepTime;
+
+        public String json;
+    }
+
+    public static class ShellySettingsInput {
+        @SerializedName("btn_type")
+        public String btnType;
+    }
+
+    public static class ShellyControlRelay {
+        // https://shelly-api-docs.shelly.cloud/#shelly1-1pm-settings-relay-0
+        @SerializedName("is_valid")
+        public Boolean isValid;
+        @SerializedName("has_timer")
+        public Boolean hasTimer; // Whether a timer is currently armed for this channel
+        @SerializedName("timer_remaining")
+        public Integer timerRemaining; // FW 1.6+
+        public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
+
+        public String turn; // Accepted values are on and off. This will turn ON/OFF the respective output
+                            // channel when request is sent .
+        public Integer timer; // A one-shot flip-back timer in seconds.
+    }
+
+    public static class ShellyShortStatusRelay {
+        public String name; // FW 1.8+: Channel could now have a logical name
+        @SerializedName("is_valid")
+        public Boolean isValid;
+        public Boolean ison; // Whether output channel is on or off
+        @SerializedName("has_timer")
+        public Boolean hasTimer; // Whether a timer is currently armed for this channel
+        @SerializedName("timer_remaining")
+        public Integer timerRemaining;
+        public Boolean overpower; // Shelly1PM only if maximum allowed power was exceeded
+        public Double temperature; // Internal device temperature
+        public Boolean overtemperature; // Device over heated
+    }
+
+    public static class ShellyShortLightStatus {
+        public Boolean ison; // Whether output channel is on or off
+        public String mode; // color or white - valid only for Bulb and RGBW2 even Dimmer returns it also
+        public Integer brightness; // brightness: 0.100%
+
+        @SerializedName("has_timer")
+        public Boolean hasTimer;
+    }
+
+    public static class ShellyStatusRelay {
+        public String name; // FW 1.8: Symbolic channel name is configurable
+
+        @SerializedName("wifi_sta")
+        public ShellySettingsWiFiNetwork wifiSta; // WiFi status
+        public ShellySettingsCoiot coiot; // Firmware 1.6+
+        public Integer serial;
+        public String mac; // MAC
+        public ArrayList<ShellyShortStatusRelay> relays; // relay status
+        public ArrayList<ShellySettingsMeter> meters; // current meter value
+        public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
+
+        @SerializedName("ext_temperature")
+        public ShellyStatusSensor.ShellyExtTemperature extTemperature; // Shelly 1/1PM: sensor values
+        @SerializedName("ext_humidity")
+        public ShellyStatusSensor.ShellyExtHumidity extHumidity; // Shelly 1/1PM: sensor values
+
+        public Double temperature; // device temp acc. on the selected temp unit
+        public ShellySensorTmp tmp;
+    }
+
+    public static class ShellyStatusDimmer {
+        @SerializedName("wifi_sta")
+        public ShellySettingsWiFiNetwork wifiSta; // WiFi status
+        public ArrayList<ShellyShortLightStatus> lights; // relay status
+        public ArrayList<ShellySettingsMeter> meters; // current meter value
+
+        public ShellySensorTmp tmp;
+        public Boolean overtemperature;
+
+        public Boolean loaderror;
+        public Boolean overload;
+    }
+
+    public static class ShellyRollerStatus {
+        public String name; // FW 1.8: Symbolic name is configurable
+
+        @SerializedName("roller_pos")
+        public Integer rollerPos; // number Desired position in percent
+        public Integer duration; // If specified, the motor will move for this period in seconds. If missing, the
+                                 // value of maxtime in /settings/roller/N will be used.
+        public String state; // One of stop, open, close
+        public Double power; // Current power consumption in Watts
+        @SerializedName("is_valid")
+        public Boolean isValid; // If the power meter functions properly
+        @SerializedName("safety_switch")
+        public Boolean safetySwitch; // Whether the safety input is currently triggered
+        public Boolean overtemperature;
+        @SerializedName("stop_reason")
+        public String stopReason; // Last cause for stopping: normal, safety_switch, obstacle
+        @SerializedName("last_direction")
+        public String lastDirection; // Last direction of motion, open or close
+        public Boolean calibrating;
+        public Boolean positioning; // true when calibration was performed
+        @SerializedName("current_pos")
+        public Integer currentPos; // current position 0..100, 100=open
+    }
+
+    public static class ShellyOtaCheckResult {
+        public String status;
+    }
+
+    public static class ShellyApRoaming {
+        public Boolean enabled;
+        public Integer threshold;
+    }
+
+    public static class ShellySensorSleepMode {
+        public Integer period;
+        public String unit;
+    }
+
+    // Shelly TRV
+    public class ShellyThermnostat {
+        public class ShellyThermTargetTemp {
+            public Boolean enabled;
+            public Double value;
+            public String unit;
+        }
+
+        public class ShellyThermTemp {
+            public Double value;
+            public String units;
+            @SerializedName("is_valid")
+            public Boolean isValid;
+        }
+
+        public Double pos;
+        @SerializedName("target_t")
+        public ShellyThermTargetTemp targetTemp;
+        public Boolean schedule;
+        @SerializedName("schedule_profile")
+        public Integer profile;
+        @SerializedName("schedule_profile_names")
+        public String[] profileNames;
+        public ShellyThermTemp tmp;
+        @SerializedName("boost_minutes")
+        public Integer boostMinutes;
+    }
+
+    public static class ShellySensorTmp {
+        public Double value; // Temperature in configured unites
+        public String units; // 'C' or 'F'
+        public Double tC; // temperature in deg C
+        public Double tF; // temperature in deg F
+        @SerializedName("is_valid")
+        public Boolean isValid; // whether the internal sensor is operating properly
+    }
+
+    public static class ShellyStatusSensor {
+        // https://shelly-api-docs.shelly.cloud/#h-amp-t-settings
+        public static class ShellySensorHum {
+            public Double value; // relative humidity in %
+        }
+
+        public static class ShellySensorBat {
+            public Double value; // estimated remaining battery capacity in %
+            public Double voltage; // battery voltage
+        };
+
+        // Door/Window sensor
+        public static class ShellySensorState {
+            @SerializedName("is_valid")
+            public Boolean isValid; // whether the internal sensor is operating properly
+            public String state; // Shelly Door/Window
+
+            // Shelly Motion
+            public Boolean motion;
+            public Boolean vibration;
+            @SerializedName("timestamp")
+            public Long motionTimestamp;
+            @SerializedName("active")
+            public Boolean motionActive;
+        }
+
+        public static class ShellySensorLux {
+            @SerializedName("is_valid")
+            public Boolean isValid; // whether the internal sensor is operating properly
+            public Double value;
+
+            public String illumination;
+        }
+
+        public static class ShellySensorAccel {
+            public Integer tilt; // Tilt in °
+            public Integer vibration; // Whether vibration is detected
+        }
+
+        public static class ShellyMotionSettings {
+            public Integer sensitivity;
+            @SerializedName("blind_time_minutes")
+            public Integer blindTimeMinutes;
+            @SerializedName("pulse_count")
+            public Integer pulseCount;
+            @SerializedName("operating_mode")
+            public Integer operatingMode;
+            public Boolean enabled;
+        }
+
+        public static class ShellyExtTemperature {
+            public static class ShellyShortTemp {
+                public Double tC; // temperature in deg C
+                public Double tF; // temperature in deg F
+            }
+
+            // Shelly 1/1PM have up to 3 sensors
+            // for whatever reasons it's not an array, but 3 independent elements
+            @SerializedName("0")
+            public ShellyShortTemp sensor1;
+            @SerializedName("1")
+            public ShellyShortTemp sensor2;
+            @SerializedName("2")
+            public ShellyShortTemp sensor3;
+        }
+
+        public static class ShellyExtHumidity {
+            public static class ShellyShortHum {
+                public Double hum; // Humidity reading of sensor 0, percent
+            }
+
+            // Shelly 1/1PM have up to 3 sensors
+            // for whatever reasons it's not an array, but 3 independent elements
+            @SerializedName("0")
+            public ShellyShortHum sensor1;
+        }
+
+        public static class ShellyADC {
+            public Double voltage;
+        }
+
+        public ShellySensorTmp tmp;
+        public ShellySensorHum hum;
+        public ShellySensorLux lux;
+        public ShellySensorAccel accel;
+        public ShellySensorBat bat;
+        @SerializedName("sensor")
+        public ShellySensorState sensor;
+        public Boolean smoke; // SHelly Smoke
+        public Boolean flood; // Shelly Flood: true = flood condition detected
+        @SerializedName("rain_sensor")
+        public Boolean rainSensor; // Shelly Flood: true=in rain mode
+
+        public Boolean motion; // Shelly Sense: true=motion detected
+        public Boolean charger; // Shelly Sense, TRV: true=charger connected
+
+        @SerializedName("act_reasons")
+        public List<Object> actReasons; // HT/Smoke/Flood: list of reasons which woke up the device
+
+        @SerializedName("sensor_error")
+        public String sensorError; // 1.5.7: Only displayed in case of error
+
+        // FW 1.7: Shelly Gas
+        @SerializedName("gas_sensor")
+        public ShellyStatusGasSensor gasSensor;
+        @SerializedName("concentration")
+        public ShellyStatusGasConcentration concentration;
+        public ArrayList<ShellyStatusValve> valves;
+
+        // FW 1.7 Button
+        @SerializedName("connect_retries")
+        public Integer connectRetries;
+        public ArrayList<ShellyInputState> inputs; // Firmware 1.5.6+
+
+        // Shelly UNI FW 1.9+
+        public ArrayList<ShellyADC> adcs;
+
+        // Shelly TRV
+        public Boolean calibrated;
+        public ArrayList<ShellyThermnostat> thermostats;
+    }
+
+    public static class ShellySettingsSmoke {
+        @SerializedName("temperature_units")
+        public String temperatureUnits; // Either 'C' or 'F'
+        @SerializedName("temperature_threshold")
+        public Integer temperatureThreshold; // Temperature delta (in configured degree units) which triggers an update
+        @SerializedName("sleep_mode_period")
+        public Integer sleepModePeriod; // Periodic update period in hours, between 1 and 24
+    }
+
+    // Shelly Gas
+    // "gas_sensor":{"sensor_state":"normal","self_test_state":"not_completed","alarm_state":"none"},
+    // "concentration":{"ppm":0,"is_valid":true},
+    public static class ShellyStatusGasSensor {
+        @SerializedName("sensor_state")
+        public String sensorState;
+        @SerializedName("self_test_state")
+        public String selfTestState;
+        @SerializedName("alarm_state")
+        public String alarmState;
+    }
+
+    public static class ShellyStatusGasConcentration {
+        public Integer ppm;
+        @SerializedName("is_valid")
+        public Boolean isValid;
+    }
+
+    public static class ShellyStatusValve {
+        public String state; // closed/opened/not_connected/failure/closing/opening/checking
+    }
+
+    public static class ShellySettingsLight {
+        public Integer red; // red brightness, 0..255, applies in mode="color"
+        public Integer green; // green brightness, 0..255, applies in mode="color"
+        public Integer blue; // blue brightness, 0..255, applies in mode="color"
+        public Integer white; // white brightness, 0..255, applies in mode="color"
+        public Integer gain; // gain for all channels, 0..100, applies in mode="color"
+        public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
+        public Integer brightness; // brightness, 0..100, applies in mode="white"
+        public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
+                               // Change, 3: Breath,
+                               // 4: Flash, 5: On/Off Gradual, 6: Red/Green Change
+        @SerializedName("default_state")
+        public String defaultState; // one of on, off or last
+        @SerializedName("auto_on")
+        public Double autoOn; // see above
+        @SerializedName("auto_off")
+        public Double autoOff; // see above
+
+        public Integer dcpower; // RGW2:Set to true for 24 V power supply, false for 12 V
+
+        // Shelly Dimmer
+        public String mode;
+        public Boolean ison;
+    }
+
+    public static class ShellySettingsNightMode { // FW1.5.7+
+        public Integer enabled;
+        @SerializedName("start_time")
+        public String startTime;
+        @SerializedName("end_time")
+        public String endTime;
+        public Integer brightness;
+    }
+
+    public static class ShellyStatusLightChannel {
+        public Boolean ison;
+        public Double power;
+        public Boolean overpower;
+        @SerializedName("has_timer")
+        public Boolean hasTimer;
+        @SerializedName("timer_started")
+        public Integer timerStarted;
+        @SerializedName("timer_duration")
+        public Integer timerDuration;
+        @SerializedName("timer_remaining")
+        public Integer timerRemaining;
+
+        public Integer red; // red brightness, 0..255, applies in mode="color"
+        public Integer green; // green brightness, 0..255, applies in mode="color"
+        public Integer blue; // blue brightness, 0..255, applies in mode="color"
+        public Integer white; // white brightness, 0..255, applies in mode="color"
+        public Integer gain; // gain for all channels, 0..100, applies in mode="color"
+        public Integer temp; // color temperature in K, 3000..6500, applies in mode="white"
+        public Integer brightness; // brightness, 0..100, applies in mode="white"
+        public Integer effect; // Currently applied effect, description: 0: Off, 1: Meteor Shower, 2: Gradual
+                               // Change, 3: Breath,
+    }
+
+    public static class ShellyStatusLight {
+        public Boolean ison; // Whether output channel is on or off
+        public Integer input;
+
+        public ArrayList<ShellyStatusLightChannel> lights;
+        public ArrayList<ShellySettingsMeter> meters;
+    }
+
+    public static class ShellySenseKeyCode {
+        String id; // ID of the stored IR code into Shelly Sense.
+        String name; // Short description or name of the stored IR code.
+    }
+
+    public static class ShellySendKeyList {
+        @SerializedName("key_codes")
+        public ArrayList<ShellySenseKeyCode> keyCodes;
+    }
+
+    /**
+     * Shelly Dimmer returns light[]. However, the structure doesn't match the lights[] of a Bulb/RGBW2.
+     * The tag lights[] will be replaced with dimmers[] so this could be mapped to a different Gson structure.
+     * The function requires that it's only called when the device is a dimmer - on get settings and get status
+     *
+     * @param json Input Json as received by the API
+     * @return Modified Json
+     */
+    public static String fixDimmerJson(String json) {
+        return !json.contains("\"lights\":[") ? json
+                : json.replaceFirst(java.util.regex.Pattern.quote("\"lights\":["), "\"dimmers\":[");
+    }
+
+    /**
+     * Convert Shelly Button events into OH button states
+     *
+     * @param eventType S/SS/SSS or L
+     * @return OH button states
+     */
+    public static String mapButtonEvent(String eventType) {
+        // decode different codings
+        // 0..2: CoAP
+        // S/SS/SSS/L: CoAP for Button and xi3
+        // shortpush/double_shortpush/triple_shortpush/longpush: REST
+        switch (eventType) {
+            case "0":
+                return CommonTriggerEvents.RELEASED;
+            case "1":
+            case SHELLY_BTNEVENT_1SHORTPUSH:
+            case SHELLY_EVENT_SHORTPUSH:
+                return CommonTriggerEvents.SHORT_PRESSED;
+            case SHELLY_BTNEVENT_2SHORTPUSH:
+            case SHELLY_EVENT_DOUBLE_SHORTPUSH:
+                return CommonTriggerEvents.DOUBLE_PRESSED;
+            case SHELLY_BTNEVENT_3SHORTPUSH:
+            case SHELLY_EVENT_TRIPLE_SHORTPUSH:
+                return "TRIPLE_PRESSED";
+            case "2":
+            case SHELLY_BTNEVENT_LONGPUSH:
+            case SHELLY_EVENT_LONGPUSH:
+                return CommonTriggerEvents.LONG_PRESSED;
+            case SHELLY_BTNEVENT_SHORTLONGPUSH:
+            case SHELLY_EVENT_SHORT_LONGTPUSH:
+                return "SHORT_LONG_PRESSED";
+            case SHELLY_BTNEVENT_LONGSHORTPUSH:
+            case SHELLY_EVENT_LONG_SHORTPUSH:
+                return "LONG_SHORT_PRESSED";
+            default:
+                return "";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTInterface.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTInterface.java
new file mode 100644 (file)
index 0000000..0be1585
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link Shelly1CoapListener} describes the listening interface to process Coap responses
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public interface Shelly1CoIoTInterface {
+    public int getVersion();
+
+    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
+
+    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
+
+    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+            Map<String, State> updates, ShellyColorUtils col);
+
+    public String getLastWakeup();
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTProtocol.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTProtocol.java
new file mode 100644 (file)
index 0000000..f544616
--- /dev/null
@@ -0,0 +1,400 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link Shelly1CoIoTProtocol} implements common functions for the CoIoT implementations
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoIoTProtocol {
+    private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTProtocol.class);
+    protected final String thingName;
+    protected final ShellyThingInterface thingHandler;
+    protected final ShellyDeviceProfile profile;
+    protected final ShellyApiInterface api;
+    protected final Map<String, CoIotDescrBlk> blkMap;
+    protected final Map<String, CoIotDescrSen> sensorMap;
+    private final Gson gson = new GsonBuilder().create();
+
+    // Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
+    // between a real update or just a repeated status on periodic updates
+    protected int lastCfgCount = -1;
+    protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
+    protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
+    protected String lastWakeup = "";
+
+    public Shelly1CoIoTProtocol(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
+            Map<String, CoIotDescrSen> sensorMap) {
+        this.thingName = thingName;
+        this.thingHandler = thingHandler;
+        this.blkMap = blkMap;
+        this.sensorMap = sensorMap;
+        this.profile = thingHandler.getProfile();
+        this.api = thingHandler.getApi();
+    }
+
+    protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
+            Map<String, State> updates, ShellyColorUtils col) {
+        // Process status information and convert into channel updates
+        int rIndex = getIdFromBlk(sen);
+        String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
+                : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
+
+        switch (sen.type.toLowerCase()) {
+            case "b": // BatteryLevel +
+                updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
+                        toQuantityType(s.value, 0, Units.PERCENT));
+                break;
+            case "h" /* Humidity */:
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
+                        toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
+                break;
+            case "m" /* Motion */:
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
+                        s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                break;
+            case "l": // Luminosity +
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
+                        toQuantityType(s.value, DIGITS_LUX, Units.LUX));
+                break;
+            case "s": // CatchAll
+                switch (sen.desc.toLowerCase()) {
+                    case "state": // Relay status +
+                    case "output":
+                        updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
+                        break;
+                    case "input":
+                        handleInput(sen, s, rGroup, updates);
+                        break;
+                    case "brightness":
+                        // already handled by state/output
+                        break;
+                    case "overtemp": // ++
+                        if (s.value == 1) {
+                            thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
+                        }
+                        break;
+                    case "position":
+                        // work around: Roller reports 101% instead max 100
+                        double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
+                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
+                                toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
+                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+                                toQuantityType(pos, Units.PERCENT));
+                        break;
+                    case "flood":
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
+                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                        break;
+                    case "vibration": // DW with FW1.6.5+
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
+                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                        if (s.value == 1) {
+                            thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
+                                    EVENT_TYPE_VIBRATION);
+                        }
+                        break;
+                    case "luminositylevel": // +
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
+                        break;
+                    case "charger": // Sense
+                        updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
+                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                        break;
+                    // RGBW2/Bulb
+                    case "red":
+                        col.setRed((int) s.value);
+                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
+                                ShellyColorUtils.toPercent((int) s.value));
+                        break;
+                    case "green":
+                        col.setGreen((int) s.value);
+                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
+                                ShellyColorUtils.toPercent((int) s.value));
+                        break;
+                    case "blue":
+                        col.setBlue((int) s.value);
+                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
+                                ShellyColorUtils.toPercent((int) s.value));
+                        break;
+                    case "white":
+                        col.setWhite((int) s.value);
+                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
+                                ShellyColorUtils.toPercent((int) s.value));
+                        break;
+                    case "gain":
+                        col.setGain((int) s.value);
+                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
+                                ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
+                        break;
+                    case "sensorerror":
+                        String sensorError = s.valueStr != null ? getString(s.valueStr) : "" + s.value;
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(sensorError));
+                        break;
+                    default:
+                        // Unknown
+                        return false;
+                }
+                break;
+
+            default:
+                // Unknown type
+                return false;
+        }
+
+        return true;
+    }
+
+    public static boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
+        updates.put(mkChannelId(group, channel), value);
+        return true;
+    }
+
+    protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
+        int idx = getSensorNumber(sen.desc, sen.id) - 1;
+        String iGroup = profile.getInputGroup(idx);
+        String iChannel = CHANNEL_INPUT + profile.getInputSuffix(idx);
+        updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
+    }
+
+    protected void handleInputEvent(CoIotDescrSen sen, String type, int count, int serial, Map<String, State> updates) {
+        int idx = getSensorNumber(sen.desc, sen.id) - 1;
+        String group = profile.getInputGroup(idx);
+        if (count == -1) {
+            // event type
+            updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(idx), new StringType(type));
+            inputEvent[idx] = type;
+        } else {
+            // event count
+            updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(idx), getDecimal(count));
+            logger.trace(
+                    "{}: Check button[{}] for event trigger (isButtonMode={}, 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.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
+                    logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
+                    thingHandler.triggerButton(group, idx, inputEvent[idx]);
+                }
+            }
+            lastEventCount[idx] = count;
+        }
+    }
+
+    /**
+     *
+     * Handles the combined updated of the brightness channel:
+     * brightness$Switch is the OnOffType (power state)
+     * brightness&Value is the brightness value
+     *
+     * @param profile Device profile, required to select the channel group and name
+     * @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
+     * @param id Sensor id from the update
+     * @param sen Sensor description from the update
+     * @param s New sensor value
+     * @param allUpdatesList of updates. This is required, because we need to update both values at the same time
+     */
+    protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
+            CoIotSensor s, List<CoIotSensor> allUpdates) {
+        String group = "";
+        String channel = CHANNEL_BRIGHTNESS;
+        String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
+        if (profile.isLight || profile.isDimmer) {
+            if (profile.isBulb || profile.inColor) {
+                group = CHANNEL_GROUP_LIGHT_CONTROL;
+                channel = CHANNEL_LIGHT_POWER;
+            } else if (profile.isDuo) {
+                group = CHANNEL_GROUP_WHITE_CONTROL;
+            } else if (profile.isDimmer) {
+                group = CHANNEL_GROUP_RELAY_CONTROL;
+            } else if (profile.isRGBW2) {
+                checkL = String.valueOf(id); // String.valueOf(id - 1); // id is 1-based, L is 0-based
+                group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
+                logger.trace("{}: updatePower() for L={}", thingName, checkL);
+            }
+
+            // We need to update brightness and on/off state at the same time to avoid "flipping brightness slider" in
+            // the UI
+            double brightness = -1.0;
+            double power = -1.0;
+            for (CoIotSensor update : allUpdates) {
+                CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
+                if (!checkL.isEmpty() && !d.links.equals(checkL)) {
+                    // continue until we find the correct one
+                    continue;
+                }
+                if (d.desc.equalsIgnoreCase("brightness")) {
+                    brightness = update.value;
+                } else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
+                    power = update.value;
+                }
+            }
+            if (power != -1) {
+                updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
+            }
+            if (brightness != -1) {
+                updateChannel(updates, group, channel + "$Value",
+                        toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, Units.PERCENT));
+            }
+        } else if (profile.hasRelays) {
+            group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
+            updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+        } else if (profile.isSensor) {
+            // Sensor state
+            if (profile.isDW) { // Door Window has item type Contact
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+                        s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+            } else {
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+                        s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+            }
+        }
+    }
+
+    /**
+     * Find index of Input id, which is required to map to channel name
+     *
+     * @parm sensorDesc D field from sensor update
+     * @param sensorId The id from the sensor update
+     * @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
+     */
+    protected int getSensorNumber(String sensorDesc, String sensorId) {
+        int idx = 0;
+        for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
+            CoIotDescrSen sen = se.getValue();
+            if (sen.desc.equalsIgnoreCase(sensorDesc)) {
+                idx++; // iterate from input1..2..n
+            }
+            if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
+                int id = getIdFromBlk(sen);
+                if (id != -1) {
+                    return id;
+                }
+            }
+            if (sen.id.equalsIgnoreCase(sensorId)) {
+                return idx;
+            }
+        }
+        logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
+        return -1;
+    }
+
+    protected int getIdFromBlk(CoIotDescrSen sen) {
+        int idx = -1;
+        CoIotDescrBlk blk = blkMap.get(sen.links);
+        if (blk != null) {
+            String desc = blk.desc.toLowerCase();
+            if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
+                    || desc.startsWith(SHELLY_CLASS_LIGHT) || desc.startsWith(SHELLY_CLASS_EMETER)) {
+                if (desc.contains("_")) { // CoAP v2
+                    idx = Integer.parseInt(substringAfter(desc, "_"));
+                } else { // CoAP v1
+                    if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
+                        idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
+                    }
+                    if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
+                        idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
+                    }
+                    if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
+                        idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
+                    }
+                }
+                idx = idx + 1; // make it 1-based (sen.L is 0-based)
+            }
+        }
+        return idx;
+    }
+
+    /**
+     *
+     * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
+     *
+     * @param sensorId sensorId to map into a channel index
+     * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
+     */
+    protected int getExtTempId(String sensorId) {
+        int idx = 0;
+        for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
+            CoIotDescrSen sen = se.getValue();
+            if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
+                    || (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
+                idx++; // iterate from temperature1..2..n
+            }
+            if (sen.id.equalsIgnoreCase(sensorId)) {
+                return idx;
+            }
+        }
+        logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
+        return -1;
+    }
+
+    protected ShellyDeviceProfile getProfile() {
+        return profile;
+    }
+
+    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
+        return sen != null ? sen : new CoIotDescrSen();
+    }
+
+    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
+    }
+
+    protected void addSensor(Map<String, CoIotDescrSen> sensorMap, String key, String json) {
+        try {
+            if (!sensorMap.containsKey(key)) {
+                CoIotDescrSen sen = gson.fromJson(json, CoIotDescrSen.class);
+                if (sen != null) {
+                    sensorMap.put(key, sen);
+                }
+            }
+        } catch (JsonSyntaxException e) {
+            // should never happen
+            logger.trace("Unable to parse sensor definition: {}", json, e);
+        }
+    }
+
+    public String getLastWakeup() {
+        return lastWakeup;
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion1.java
new file mode 100644 (file)
index 0000000..9eb440a
--- /dev/null
@@ -0,0 +1,375 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 1
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoIoTVersion1 extends Shelly1CoIoTProtocol implements Shelly1CoIoTInterface {
+    private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTVersion1.class);
+
+    public Shelly1CoIoTVersion1(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
+            Map<String, CoIotDescrSen> sensorMap) {
+        super(thingName, thingHandler, blkMap, sensorMap);
+    }
+
+    @Override
+    public int getVersion() {
+        return Shelly1CoapJSonDTO.COIOT_VERSION_1;
+    }
+
+    /**
+     * Process CoIoT status update message. If a status update is received, but the device description has not been
+     * received yet a GET is send to query device description.
+     *
+     * @param devId device id included in the status packet
+     * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
+     * @param serial Serial for this request. If this the the same as last serial
+     *            the update was already sent and processed so this one gets
+     *            ignored.
+     */
+    @Override
+    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+            Map<String, State> updates, ShellyColorUtils col) {
+        // first check the base implementation
+        if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
+            // process by the base class
+            return true;
+        }
+
+        // Process status information and convert into channel updates
+        Integer rIndex = Integer.parseInt(sen.links) + 1;
+        String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
+                : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
+        switch (sen.type.toLowerCase()) {
+            case "t": // Temperature +
+                Double value = getDouble(s.value);
+                switch (sen.desc.toLowerCase()) {
+                    case "temperature": // Sensor Temp
+                        if (getString(getProfile().settings.temperatureUnits)
+                                .equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
+                            value = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(s.value))
+                                    .doubleValue();
+                        }
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
+                                toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+                        break;
+                    case "temperature f": // Device Temp -> ignore (we use C only)
+                        break;
+                    case "temperature c": // Device Temp in C
+                        // Device temperature
+                        updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
+                                toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
+                        break;
+                    case "external temperature f": // Shelly 1/1PM external temp sensors
+                        // ignore F, we use C only
+                        break;
+                    case "external temperature c": // Shelly 1/1PM external temp sensors
+                    case "external_temperature":
+                        int idx = getExtTempId(sen.id);
+                        if (idx > 0) {
+                            updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
+                                    toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+                        } else {
+                            logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
+                                    sen.desc);
+                        }
+                        break;
+                    default:
+                        logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
+                }
+                break;
+            case "p": // Power/Watt
+                // 3EM uses 1-based meter IDs, other 0-based
+                String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
+                        : CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
+                updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
+                        toQuantityType(s.value, DIGITS_WATT, Units.WATT));
+                updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
+                break;
+            case "s" /* CatchAll */:
+                switch (sen.desc.toLowerCase()) {
+                    case "overtemp":
+                        if (s.value == 1) {
+                            thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
+                        }
+                        break;
+                    case "energy counter 0 [w-min]":
+                        updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
+                                toQuantityType(s.value, DIGITS_WATT, Units.WATT));
+                        break;
+                    case "energy counter 1 [w-min]":
+                    case "energy counter 2 [w-min]":
+                        // we don't use them
+                        break;
+                    case "energy counter total [w-h]": // 3EM reports W/h
+                    case "energy counter total [w-min]":
+                        Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
+                        updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
+                                toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
+                        break;
+                    case "voltage":
+                        updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
+                                toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
+                        break;
+                    case "current":
+                        updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
+                                toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
+                        break;
+                    case "pf":
+                        updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR,
+                                toQuantityType(getDecimal(s.value), Units.PERCENT));
+                        break;
+                    case "position":
+                        // work around: Roller reports 101% instead max 100
+                        double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
+                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
+                                toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
+                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+                                toQuantityType(pos, Units.PERCENT));
+                        break;
+                    case "input event": // Shelly Button 1
+                        handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
+                        break;
+                    case "input event counter": // Shelly Button 1/ix3
+                        handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
+                        break;
+                    case "flood":
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
+                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                        break;
+                    case "tilt": // DW with FW1.6.5+ //+
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
+                                toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
+                        break;
+                    case "vibration": // DW with FW1.6.5+
+                        if (profile.isMotion) {
+                            // handle as status
+                            updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
+                                    s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                        } else if (s.value == 1) {
+                            // handle as event
+                            thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
+                                    EVENT_TYPE_VIBRATION);
+                        }
+                        break;
+                    case "temp": // Shelly Bulb
+                    case "colortemperature": // Shelly Duo
+                        updateChannel(updates,
+                                profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
+                                CHANNEL_COLOR_TEMP,
+                                ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
+                        break;
+                    case "sensor state": // Shelly Gas
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
+                        break;
+                    case "alarm state": // Shelly Gas
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
+                                getStringType(s.valueStr));
+                        break;
+                    case "self-test state":// Shelly Gas
+                        updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
+                                getStringType(s.valueStr));
+                        break;
+                    case "concentration":// Shelly Gas
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
+                                toQuantityType(getDouble(s.value), DIGITS_NONE, Units.PARTS_PER_MILLION));
+                        break;
+                    case "sensorerror":
+                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
+                        break;
+                    default:
+                        // Unknown
+                        return false;
+                }
+                break;
+
+            default:
+                // Unknown type
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     *
+     * Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
+     * Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
+     * that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
+     * old mapping stays in to support older firmware releases.
+     *
+     * @param sen Sensor description received from device
+     * @return fixed Sensor description (sen)
+     */
+    @Override
+    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
+        // Shelly1: reports null descr+type "Switch" -> map to S
+        // Shelly1PM: reports null descr+type "Overtemp" -> map to O
+        // Shelly1PM: reports null descr+type "W" -> add description
+        // Shelly1PM: reports temp senmsors without desc -> add description
+        // Shelly Dimmer: sensors are reported without descriptions -> map to S
+        // SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
+        // Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
+        // Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
+        // Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
+        // Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
+        if (sen == null) {
+            throw new IllegalArgumentException("sen should not be null!");
+        }
+        if (sen.desc == null) {
+            sen.desc = "";
+        }
+        String desc = sen.desc.toLowerCase();
+
+        // RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
+        // we break it up to Power with L:0, Power with L:1...
+        if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
+            String newDesc = substringBefore(sen.desc, "_");
+            String newLink = substringAfter(sen.desc, "_");
+            sen.desc = newDesc;
+            sen.links = newLink;
+            if (!blkMap.containsKey(sen.links)) {
+                // auto-insert a matching blk entry
+                CoIotDescrBlk blk = new CoIotDescrBlk();
+                CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
+                blk.id = sen.links;
+                if (blk0 != null) {
+                    blk.desc = blk0.desc + "_" + blk.id;
+                    blkMap.put(blk.id, blk);
+                }
+            }
+        }
+
+        switch (sen.type.toLowerCase()) {
+            case "w": // old devices/firmware releases use "W", new ones "P"
+                sen.type = "P";
+                sen.desc = "Power";
+                break;
+            case "tc":
+                sen.type = "T";
+                sen.desc = "Temperature C";
+                break;
+            case "tf":
+                sen.type = "T";
+                sen.desc = "Temperature F";
+                break;
+            case "overtemp":
+                sen.type = "S";
+                sen.desc = "Overtemp";
+                break;
+            case "relay0":
+            case "switch":
+            case "vswitch":
+                sen.type = "S";
+                sen.desc = "State";
+                break;
+        }
+
+        switch (sen.desc.toLowerCase()) {
+            case "motion": // fix acc to spec it's T=M
+                sen.type = "M";
+                sen.desc = "Motion";
+                break;
+            case "battery": // fix: type is B not H
+                sen.type = "B";
+                sen.desc = "Battery";
+                break;
+            case "overtemp":
+                sen.type = "S";
+                sen.desc = "Overtemp";
+                break;
+            case "relay0":
+            case "switch":
+            case "vswitch":
+                sen.type = "S";
+                sen.desc = "State";
+                break;
+            case "e cnt 0 [w-min]": // 4 Pro
+            case "e cnt 1 [w-min]":
+            case "e cnt 2 [w-min]":
+            case "e cnt total [w-min]": // 4 Pro
+                sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
+                break;
+
+        }
+
+        if (sen.desc.isEmpty()) {
+            switch (sen.type.toLowerCase()) {
+                case "p":
+                    sen.desc = "Power";
+                    break;
+                case "T":
+                    sen.desc = "Temperature";
+                    break;
+                case "input":
+                    sen.type = "S";
+                    sen.desc = "Input";
+                    break;
+                case "output":
+                    sen.type = "S";
+                    sen.desc = "Output";
+                    break;
+                case "brightness":
+                    sen.type = "S";
+                    sen.desc = "Brightness";
+                    break;
+                case "red":
+                case "green":
+                case "blue":
+                case "white":
+                case "gain":
+                case "temp": // Bulb: Color temperature
+                    sen.desc = sen.type;
+                    sen.type = "S";
+                    break;
+                case "vswitch":
+                    // it seems that Shelly tends to break their own spec: T is the description and D is no longer
+                    // included -> map D to sen.T and set CatchAll for T
+                    sen.desc = sen.type;
+                    sen.type = "S";
+                    break;
+                // Default: set no description
+                // (there are no T values defined in the CoIoT spec)
+                case "tostate":
+                default:
+                    sen.desc = "";
+            }
+        }
+        return sen;
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion2.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoIoTVersion2.java
new file mode 100644 (file)
index 0000000..432ff38
--- /dev/null
@@ -0,0 +1,416 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+/**
+ * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 1
+ *
+ * @author Markus Michels - Initial contribution
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.OpenClosedType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Shelly1CoIoTVersion1} implements the parsing for CoIoT version 2
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoIoTVersion2 extends Shelly1CoIoTProtocol implements Shelly1CoIoTInterface {
+    private final Logger logger = LoggerFactory.getLogger(Shelly1CoIoTVersion2.class);
+
+    public Shelly1CoIoTVersion2(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
+            Map<String, CoIotDescrSen> sensorMap) {
+        super(thingName, thingHandler, blkMap, sensorMap);
+    }
+
+    @Override
+    public int getVersion() {
+        return Shelly1CoapJSonDTO.COIOT_VERSION_2;
+    }
+
+    /**
+     * Process CoIoT status update message. If a status update is received, but the device description has not been
+     * received yet a GET is send to query device description.
+     *
+     * @param sensorUpdates Complete list of sensor updates
+     * @param sen The specific sensor update to handle
+     * @param updates Resulting updates (new updates will be added to input list)
+     */
+    @Override
+    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
+            Map<String, State> updates, ShellyColorUtils col) {
+        // first check the base implementation
+        if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
+            // process by the base class
+            return true;
+        }
+
+        // Process status information and convert into channel updates
+        // Integer rIndex = Integer.parseInt(sen.links) + 1;
+        int rIndex = getIdFromBlk(sen);
+        String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
+                : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
+        String mGroup = profile.numMeters <= 1 ? CHANNEL_GROUP_METER
+                : CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
+
+        boolean processed = true;
+        double value = getDouble(s.value);
+        String reason = "";
+
+        if (profile.isTRV) {
+            // Special handling for TRV, because it uses duplicate ID values with different meanings
+            switch (sen.id) {
+                case "3101": // current temp
+                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
+                            toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+                    break;
+                case "3103": // target temp in C. 4/31, 999=unknown
+                    updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
+                            toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+                    break;
+                case "3116": // S, valveError, 0/1
+                    if (s.value == 1) {
+                        thingHandler.postEvent(ALARM_TYPE_VALVE_ERROR, false);
+                    }
+                    break;
+                case "3117": // S, mode, 0-5 (0=disabled)
+                    value = getDouble(s.value).intValue();
+                    updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE, getDecimal(value));
+                    updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SCHEDULE, getOnOff(value > 0));
+                    break;
+                case "3118": // Valve state
+                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+                            value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+                    break;
+                case "3121": // valvePos, Type=S, Range=0/100;
+                    boolean updated = updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
+                            s.value != -1 ? toQuantityType(getDouble(s.value), 0, Units.PERCENT) : UnDefType.UNDEF);
+                    if (updated && s.value >= 0 && s.value != thingHandler.getChannelDouble(CHANNEL_GROUP_CONTROL,
+                            CHANNEL_CONTROL_POSITION)) {
+                        logger.debug("{}: Valve position changed, force update", thingName);
+                        thingHandler.requestUpdates(1, false);
+                    }
+                    break;
+                default:
+                    processed = false;
+            }
+        } else {
+            processed = false;
+        }
+
+        if (processed) {
+            return true;
+        }
+
+        processed = true;
+        switch (sen.id) {
+            case "3106": // L, luminosity, lux, U32, -1
+            case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
+            case "3111": // B, battery, 0-100%, unknown -1
+            case "3112": // S, charger, 0/1
+            case "3115": // S, sensorError, 0/1
+                // processed by base handler
+                break;
+
+            case "6109": // P, overpowerValue, W, U32
+            case "9101":
+                // Relay: S, mode, relay/roller or
+                // Dimmer: S, mode, color/white
+                // skip, could check against thing mode...
+                break;
+
+            case "1101": // relay_0: output, 0/1
+            case "1201": // relay_1: output, 0/1
+            case "1301": // relay_2: output, 0/1
+            case "1401": // relay_3: output, 0/1
+                updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
+                break;
+            case "1102": // roler_0: S, roller, open/close/stop -> roller state
+                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
+                break;
+            case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
+                int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
+                logger.debug("{}: CoAP update roller position: control={}, position={}", thingName,
+                        SHELLY_MAX_ROLLER_POS - pos, pos);
+                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
+                        toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
+                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
+                        toQuantityType((double) pos, Units.PERCENT));
+                break;
+            case "1105": // Gas: S, valve, closed/opened/not_connected/failure/closing/opening/checking or unknown
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
+                break;
+
+            case "2101": // Input_0: S, input, 0/1
+            case "2201": // Input_1: S, input, 0/1
+            case "2301": // Input_2: S, input, 0/1
+            case "2401": // Input_3: S, input, 0/1
+                handleInput(sen, s, rGroup, updates);
+                break;
+            case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
+            case "2202": // Input_1: EV, inputEvent
+            case "2302": // Input_2: EV, inputEvent
+            case "2402": // Input_3: EV, inputEvent
+                handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
+                break;
+            case "2103": // EVC, inputEventCnt, U16
+            case "2203": // EVC, inputEventCnt, U16
+            case "2303": // EVC, inputEventCnt, U16
+            case "2403": // EVC, inputEventCnt, U16
+                handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
+                break;
+            case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
+            case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
+            case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
+                int idx = getExtTempId(sen.id);
+                if (idx >= 0) {
+                    // H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
+                    String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
+                    // Some devices report values = -999 or 99 during fw update
+                    updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
+                            toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
+                } else {
+                    logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
+                }
+                break;
+            case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
+                if ("targetTemp".equalsIgnoreCase(sen.desc)) {
+
+                    break; // target temp in F-> ignore
+                }
+                // sensor_0: T, internalTemp, F, 39/88, unknown 999
+                updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
+                        toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
+                break;
+            case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
+            case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
+            case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
+            case "3105": // T, deviceTemp, Fahrenheit -40/572
+                // skip, we use only C
+                break;
+
+            case "3107": // C, Gas concentration, U16
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
+                break;
+            case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
+                if (value != -1) {
+                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
+                            value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+                } else {
+                    logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
+                }
+                break;
+            case "3109": // S, tilt, 0-180deg, -1
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
+                        toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
+                break;
+            case "3113": // S, sensorOp, warmup/normal/fault
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
+                break;
+            case "3114": // S, selfTest, not_completed/completed/running/pending
+                updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
+                break;
+            case "3117": // S, extInput, 0/1
+                handleInput(sen, s, rGroup, updates);
+                break;
+            case "3118":
+                updateChannel(updates, mGroup, CHANNEL_SENSOR_VOLTAGE,
+                        toQuantityType(getDouble(s.value), 2, Units.VOLT));
+                break;
+
+            case "4101": // relay_0/light_0: P, power, W
+            case "4201": // relay_1/light_1: P, power, W
+            case "4301": // relay_2/light_2: P, power, W
+            case "4401": // relay_3/light_3: P, power, W
+            case "4105": // emeter_0: P, power, W
+            case "4205": // emeter_1: P, power, W
+            case "4305": // emeter_2: P, power, W
+            case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
+            case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
+                updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
+                        toQuantityType(s.value, DIGITS_WATT, Units.WATT));
+                if (!profile.isRGBW2 && !profile.isRoller) {
+                    // only for regular, not-aggregated meters
+                    updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
+                }
+                break;
+
+            case "4103": // relay_0: E, energy, Wmin, U32
+            case "4203": // relay_1: E, energy, Wmin, U32
+            case "4303": // relay_2: E, energy, Wmin, U32
+            case "4403": // relay_3: E, energy, Wmin, U32
+            case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
+            case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
+            case "4106": // emeter_0: E, energy, Wh, U32
+            case "4206": // emeter_1: E, energy, Wh, U32
+            case "4306": // emeter_2: E, energy, Wh, U32
+                double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
+                updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
+                        toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
+                break;
+
+            case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
+            case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
+            case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
+                updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
+                        toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
+                break;
+
+            case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
+            case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
+            case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
+                updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
+                        toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
+                break;
+
+            case "4109": // emeter_0: A, current, 0/120A, -1
+            case "4209": // emeter_1: A, current, 0/120A, -1
+            case "4309": // emeter_2: A, current, 0/120A, -1
+                updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
+                        toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
+                break;
+
+            case "4110": // emeter_0: S, powerFactor, 0/1, -1
+            case "4210": // emeter_1: S, powerFactor, 0/1, -1
+            case "4310": // emeter_2: S, powerFactor, 0/1, -1
+                updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
+                break;
+
+            case "5101": // {"I":5101,"T":"S","D":"brightness","R":"0/100","L":1},
+            case "5102": // {"I":5102,"T":"S","D":"gain","R":"0/100","L":1},
+            case "5103": // {"I":5103,"T":"S","D":"colorTemp","U":"K","R":"3000/6500","L":1},
+            case "5105": // {"I":5105,"T":"S","D":"red","R":"0/255","L":1},
+            case "5106": // {"I":5106,"T":"S","D":"green","R":"0/255","L":1},
+            case "5107": // {"I":5107,"T":"S","D":"blue","R":"0/255","L":1},
+            case "5108": // {"I":5108,"T":"S","D":"white","R":"0/255","L":1},
+                // already covered by base handler
+                break;
+
+            case "6101": // A, overtemp, 0/1
+                if (s.value == 1) {
+                    thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
+                }
+                break;
+            case "6102": // relay_0: A, overpower, 0/1
+            case "6202": // relay_1: A, overpower, 0/1
+            case "6302": // relay_2: A, overpower, 0/1
+            case "6402": // relay_3: A, overpower, 0/1
+                if (s.value == 1) {
+                    thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
+                }
+                break;
+            case "6104": // relay_0: A, loadError, 0/1
+            case "6204": // relay_1: A, loadError, 0/1
+            case "6304": // relay_2: A, loadError, 0/1
+            case "6404": // relay_3: A, loadError, 0/1
+                if (s.value == 1) {
+                    thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
+                }
+                break;
+            case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
+                reason = getString(s.valueStr);
+                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
+                if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
+                    thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
+                }
+            case "6106": // A, flood, 0/1, -1
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
+                        value == 1 ? OnOffType.ON : OnOffType.OFF);
+                break;
+
+            case "6107": // A, motion, 0/1, -1
+                // {"I":6107,"T":"A","D":"motion","R":["0/1","-1"],"L":1},
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
+                        value == 1 ? OnOffType.ON : OnOffType.OFF);
+                break;
+            case "3119": // Motion timestamp (timestamp os GMT, not adapted to the adapted timezone)
+                // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
+                if (s.value != 0) {
+                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
+                            getTimestamp(getString("GMT"), (long) s.value));
+                }
+                break;
+            case "3120": // motionActive (timestamp os GMT, not adapted to the adapted timezone)
+                // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
+                        getTimestamp("GMT", (long) s.value));
+                break;
+
+            case "6108": // A, gas, none/mild/heavy/test or unknown
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
+                break;
+            case "6110": // A, vibration, 0/1, -1=unknown
+                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
+                        s.value == 1 ? OnOffType.ON : OnOffType.OFF);
+                if (s.value == 1) {
+                    // post event
+                    thingHandler.triggerChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM, EVENT_TYPE_VIBRATION);
+                }
+                break;
+            case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
+                if (s.valueArray.size() > 0) {
+                    thingHandler.updateWakeupReason(s.valueArray);
+                    lastWakeup = (String) s.valueArray.get(0);
+                }
+                break;
+            case "9103": // EVC, cfgChanged, U16
+                if (lastCfgCount == -1 || lastCfgCount != s.value) {
+                    thingHandler.requestUpdates(1, true); // refresh config
+                }
+                lastCfgCount = (int) s.value;
+                break;
+
+            default:
+                processed = false;
+        }
+        return processed;
+    }
+
+    @Override
+    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
+        return super.fixDescription(sen, blkMap);
+    }
+
+    private static final String ID_4101_DESCR = "{ \"I\":4101, \"T\":\"P\", \"D\":\"power\",  \"U\": \"W\",    \"R\":\"0/3500\", \"L\": 1}";
+    private static final String ID_4103_DESCR = "{ \"I\":4103, \"T\":\"E\", \"D\":\"energy\", \"U\": \"Wmin\", \"R\":\"U32\", \"L\": 1}";
+
+    @Override
+    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
+        if (profile.isDuo && profile.inColor) {
+            addSensor(sensorMap, "4101", ID_4101_DESCR);
+            addSensor(sensorMap, "4103", ID_4103_DESCR);
+        }
+        super.completeMissingSensorDefinition(sensorMap);
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapHandler.java
new file mode 100644 (file)
index 0000000..bd0889b
--- /dev/null
@@ -0,0 +1,681 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.coap.CoAP.Code;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.CoAP.Type;
+import org.eclipse.californium.core.coap.MessageObserverAdapter;
+import org.eclipse.californium.core.coap.Option;
+import org.eclipse.californium.core.coap.OptionNumberRegistry;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.Endpoint;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrBlk;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDescrSen;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDevDescrTypeAdapter;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotDevDescription;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotGenericSensorList;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensor;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.CoIotSensorTypeAdapter;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link Shelly1CoapHandler} handles the CoIoT/CoAP registration and events.
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoapHandler implements Shelly1CoapListener {
+    private static final byte[] EMPTY_BYTE = new byte[0];
+
+    private final Logger logger = LoggerFactory.getLogger(Shelly1CoapHandler.class);
+    private final ShellyThingInterface thingHandler;
+    private ShellyThingConfiguration config = new ShellyThingConfiguration();
+    private final GsonBuilder gsonBuilder = new GsonBuilder();
+    private final Gson gson;
+    private String thingName;
+
+    private boolean coiotBound = false;
+    private Shelly1CoIoTInterface coiot;
+    private int coiotVers = -1;
+
+    private final Shelly1CoapServer coapServer;
+    private @Nullable CoapClient statusClient;
+    private Request reqDescription = new Request(Code.GET, Type.CON);
+    private Request reqStatus = new Request(Code.GET, Type.CON);
+    private boolean updatesRequested = false;
+    private int coiotPort = COIOT_PORT;
+
+    private long coiotMessages = 0;
+    private long coiotErrors = 0;
+    private int lastSerial = -1;
+    private String lastPayload = "";
+    private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
+    private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
+    private ShellyDeviceProfile profile;
+    private ShellyApiInterface api;
+
+    public Shelly1CoapHandler(ShellyThingInterface thingHandler, Shelly1CoapServer coapServer) {
+        this.thingHandler = thingHandler;
+        this.thingName = thingHandler.getThingName();
+        this.profile = thingHandler.getProfile();
+        this.api = thingHandler.getApi();
+        this.coapServer = coapServer;
+        this.coiot = new Shelly1CoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
+
+        gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
+        gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
+        gson = gsonBuilder.create();
+    }
+
+    /**
+     * Initialize CoAP access, send discovery packet and start Status server
+     *
+     * @parm thingName Thing name derived from Thing Type/hostname
+     * @parm config ShellyThingConfiguration
+     * @thows ShellyApiException
+     */
+    public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
+        try {
+            this.thingName = thingName;
+            this.config = config;
+            this.profile = thingHandler.getProfile();
+            if (isStarted()) {
+                logger.trace("{}: CoAP Listener was already started", thingName);
+                stop();
+            }
+
+            logger.debug("{}: Starting CoAP Listener", thingName);
+            if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) {
+                String ps = substringAfter(profile.coiotEndpoint, ":");
+                coiotPort = Integer.parseInt(ps);
+            }
+            coapServer.start(config.localIp, coiotPort, this);
+            statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS))
+                    .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
+            @Nullable
+            Endpoint endpoint = null;
+            CoapClient client = statusClient;
+            if (client != null) {
+                endpoint = client.getEndpoint();
+            }
+            if ((endpoint == null) || !endpoint.isStarted()) {
+                logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
+                throw new ShellyApiException("Network initialization failed");
+            }
+
+            discover();
+        } catch (SocketException e) {
+            logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
+            throw new ShellyApiException("Network error", e);
+        } catch (UnknownHostException e) {
+            logger.info("{}: CoAP Exception (Unknown Host)", thingName, e);
+            throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
+        }
+    }
+
+    public boolean isStarted() {
+        return statusClient != null;
+    }
+
+    /**
+     * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
+     *
+     * @param response The Response packet
+     */
+    @Override
+    public void processResponse(@Nullable Response response) {
+        if (response == null) {
+            coiotErrors++;
+            return; // other device instance
+        }
+        ResponseCode code = response.getCode();
+        if (code != ResponseCode.CONTENT) {
+            // error handling
+            logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
+                    response.getPayloadString());
+            coiotErrors++;
+            return;
+        }
+
+        List<Option> options = response.getOptions().asSortedList();
+        String ip = response.getSourceContext().getPeerAddress().toString();
+        boolean match = ip.contains(config.deviceIp);
+        if (!match) {
+            // We can't identify device by IP, so we need to check the CoAP header's Global Device ID
+            for (Option opt : options) {
+                if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
+                    String devid = opt.getStringValue();
+                    if (devid.contains("#")) {
+                        // Format: <device type>#<mac address>#<coap version>
+                        String macid = substringBetween(devid, "#", "#");
+                        if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
+                            match = true;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        if (!match) {
+            // other instance
+            return;
+        }
+
+        String payload = "";
+        String devId = "";
+        String uri = "";
+        int serial = -1;
+        try {
+            coiotMessages++;
+            if (logger.isDebugEnabled()) {
+                logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
+                        response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
+            }
+            if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
+                logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
+                coiotErrors++;
+                return;
+            }
+
+            payload = response.getPayloadString();
+            for (Option opt : options) {
+                switch (opt.getNumber()) {
+                    case OptionNumberRegistry.URI_PATH:
+                        uri = COLOIT_URI_BASE + opt.getStringValue();
+                        break;
+                    case OptionNumberRegistry.URI_HOST: // ignore
+                        break;
+                    case OptionNumberRegistry.CONTENT_FORMAT: // ignore
+                        break;
+                    case COIOT_OPTION_GLOBAL_DEVID:
+                        devId = opt.getStringValue();
+                        String sVersion = substringAfterLast(devId, "#");
+                        int iVersion = Integer.parseInt(sVersion);
+                        if (coiotBound && (coiotVers != iVersion)) {
+                            logger.debug("{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
+                                    thingName, coiotVers, iVersion);
+                            thingHandler.reinitializeThing();
+                            coiotBound = false;
+                        }
+                        if (!coiotBound) {
+                            thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
+                            logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
+                            if (iVersion == COIOT_VERSION_1) {
+                                coiot = new Shelly1CoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
+                            } else if (iVersion == COIOT_VERSION_2) {
+                                coiot = new Shelly1CoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
+                            } else {
+                                logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
+                                return;
+                            }
+                            coiotVers = iVersion;
+                            coiotBound = true;
+                        }
+                        break;
+                    case COIOT_OPTION_STATUS_VALIDITY:
+                        break;
+                    case COIOT_OPTION_STATUS_SERIAL:
+                        serial = opt.getIntegerValue();
+                        break;
+                    default:
+                        logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
+                                opt.getValue());
+                }
+            }
+
+            // If we received a CoAP message successful the thing must be online
+            thingHandler.setThingOnline();
+
+            // The device changes the serial on every update, receiving a message with the same serial is a
+            // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
+            if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
+                    || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
+                logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
+                return;
+            }
+
+            // fixed malformed JSON :-(
+            payload = fixJSON(payload);
+
+            try {
+                if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
+                    handleDeviceDescription(devId, payload);
+                } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
+                        || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
+                    handleStatusUpdate(devId, payload, serial);
+                }
+            } catch (ShellyApiException e) {
+                logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
+                coiotErrors++;
+            }
+
+            if (!updatesRequested) {
+                // Observe Status Updates
+                reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
+                updatesRequested = true;
+            }
+        } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
+            logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
+            resetSerial();
+            coiotErrors++;
+        }
+    }
+
+    /**
+     * Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
+     * etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
+     * status updates to the device units and matching the correct thing channel.
+     *
+     * @param devId The device id reported in the CoIoT message.
+     * @param payload Device desciption in JSon format, example:
+     *            {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
+     */
+    private void handleDeviceDescription(String devId, String payload) throws ShellyApiException {
+        logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
+
+        try {
+            boolean valid = true;
+
+            // Decode Json
+            CoIotDevDescription descr = fromJson(gson, payload, CoIotDevDescription.class);
+            for (int i = 0; i < descr.blk.size(); i++) {
+                CoIotDescrBlk blk = descr.blk.get(i);
+                logger.debug("{}:    id={}: {}", thingName, blk.id, blk.desc);
+                if (!blkMap.containsKey(blk.id)) {
+                    blkMap.put(blk.id, blk);
+                } else {
+                    blkMap.replace(blk.id, blk);
+                }
+                if ((blk.type != null) && !blk.type.isEmpty()) {
+                    // in fact it is a sen entry - that's vioaling the Spec
+                    logger.trace("{}:    fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
+                            blk.desc);
+                    CoIotDescrSen sen = new CoIotDescrSen();
+                    sen.id = blk.id;
+                    sen.desc = blk.desc;
+                    sen.type = blk.type;
+                    sen.range = blk.range;
+                    sen.links = blk.links;
+                    valid &= addSensor(sen);
+                }
+            }
+
+            // Save to thing properties
+            thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
+
+            logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
+            if (descr.sen != null) {
+                for (int i = 0; i < descr.sen.size(); i++) {
+                    valid &= addSensor(descr.sen.get(i));
+                }
+            }
+            coiot.completeMissingSensorDefinition(sensorMap);
+
+            if (!valid) {
+                logger.debug(
+                        "{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
+                        thingName, coiot.getVersion());
+
+                discover();
+                return;
+            }
+        } catch (JsonSyntaxException e) {
+            logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
+        } catch (NullPointerException | IllegalArgumentException e) {
+            logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
+        }
+    }
+
+    /**
+     * Add a new sensor to the sensor table
+     *
+     * @param sen CoIotDescrSen of the sensor
+     */
+    private synchronized boolean addSensor(CoIotDescrSen sen) {
+        logger.debug("{}:    id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
+                sen.links);
+        // CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
+        // We need to make sure that the persisted device description matches,
+        // otherwise the stored one is discarded and a new discovery is triggered
+        // This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
+        int vers = coiot.getVersion();
+        if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
+                || ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
+            logger.debug("{}: Invalid format for sensor defition detected, id={}", thingName, sen.id);
+            return false;
+        }
+
+        try {
+            CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
+            if (!sensorMap.containsKey(fixed.id)) {
+                sensorMap.put(sen.id, fixed);
+            } else {
+                sensorMap.replace(sen.id, fixed);
+            }
+        } catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
+                                                                      // description is buggy
+            logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
+        }
+
+        return true;
+    }
+
+    /**
+     * Process CoIoT status update message. If a status update is received, but the device description has not been
+     * received yet a GET is send to query device description.
+     *
+     * @param devId device id included in the status packet
+     * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
+     * @param serial Serial for this request. If this the the same as last serial
+     *            the update was already sent and processed so this one gets
+     *            ignored.
+     * @throws ShellyApiException
+     */
+    private void handleStatusUpdate(String devId, String payload, int serial) throws ShellyApiException {
+        logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
+        if (blkMap.isEmpty()) {
+            // send discovery packet
+            resetSerial();
+            discover();
+
+            // try to uses description from last initialization
+            String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
+            if (savedDescr.isEmpty()) {
+                logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
+                return;
+            }
+
+            // simulate received device description to create element table
+            logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
+            handleDeviceDescription(devId, savedDescr);
+        }
+
+        // Parse Json,
+        CoIotGenericSensorList list = fromJson(gson, fixJSON(payload), CoIotGenericSensorList.class);
+        if (list.generic == null) {
+            logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
+            return;
+        }
+
+        List<CoIotSensor> sensorUpdates = list.generic;
+        Map<String, State> updates = new TreeMap<String, State>();
+        logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
+        int failed = 0;
+        ShellyColorUtils col = new ShellyColorUtils();
+        for (int i = 0; i < sensorUpdates.size(); i++) {
+            try {
+                CoIotSensor s = sensorUpdates.get(i);
+                CoIotDescrSen sen = sensorMap.get(s.id);
+                if (sen == null) {
+                    logger.debug("{}: Unable to sensor definition for id={}, payload={}", thingName, s.id, payload);
+                    continue;
+                }
+                // find matching sensor definition from device description, use the Link ID as index
+                CoIotDescrBlk element = null;
+                sen = coiot.fixDescription(sen, blkMap);
+                element = blkMap.get(sen.links);
+                if (element == null) {
+                    logger.debug("{}: Unable to find BLK for link {} from sen.id={}, payload={}", thingName, sen.links,
+                            sen.id, payload);
+                    continue;
+                }
+                logger.trace("{}:  Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
+                        i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
+                        sen.links, element.desc);
+
+                if (!coiot.handleStatusUpdate(sensorUpdates, sen, serial, s, updates, col)) {
+                    logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
+                            sen.id, sen.type, sen.desc, s.value, payload);
+                }
+            } catch (NullPointerException | IllegalArgumentException e) {
+                // even the processing of one value failed we continue with the next one (sometimes this is caused by
+                // buggy formats provided by the device
+                logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
+                        payload, e);
+            }
+        }
+
+        if (!updates.isEmpty()) {
+            int updated = 0;
+            for (Map.Entry<String, State> u : updates.entrySet()) {
+                String key = u.getKey();
+                updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
+            }
+            if (updated > 0) {
+                logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
+                if (profile.isSensor || profile.isRoller) {
+                    // CoAP is currently lacking the lastUpdate info, so we use host timestamp
+                    thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
+                }
+            }
+
+            if (profile.isLight && profile.inColor && col.isRgbValid()) {
+                // Update color picker from single values
+                if (col.isRgbValid()) {
+                    thingHandler.updateChannel(mkChannelId(CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_PICKER),
+                            col.toHSB(), false);
+                }
+            }
+
+            if ((profile.isRGBW2 && !profile.inColor) || profile.isRoller) {
+                // Aggregate Meter Data from different Coap updates
+                int i = 1;
+                double totalCurrent = 0.0;
+                double totalKWH = 0.0;
+                boolean updateMeter = false;
+                while (i <= thingHandler.getProfile().numMeters) {
+                    String meter = CHANNEL_GROUP_METER + i;
+                    double current = thingHandler.getChannelDouble(meter, CHANNEL_METER_CURRENTWATTS);
+                    double total = thingHandler.getChannelDouble(meter, CHANNEL_METER_TOTALKWH);
+                    totalCurrent += current >= 0 ? current : 0;
+                    totalKWH += total >= 0 ? total : 0;
+                    updateMeter |= current >= 0 | total >= 0;
+                    i++;
+                }
+                if (updateMeter) {
+                    thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_METER_CURRENTWATTS,
+                            toQuantityType(totalCurrent, DIGITS_WATT, Units.WATT));
+                    thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_LAST_UPDATE, getTimestamp());
+                }
+            }
+
+            // Old firmware release are lacking various status values, which are not updated using CoIoT.
+            // In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
+            // of the values are available
+            thingHandler.triggerUpdateFromCoap();
+        } else {
+            if (failed == sensorUpdates.size()) {
+                logger.debug("{}: Device description problem detected, re-discover", thingName);
+                coiotBound = false;
+                discover();
+            }
+        }
+
+        // Remember serial, new packets with same serial will be ignored
+        lastSerial = serial;
+        lastPayload = payload;
+    }
+
+    private void discover() {
+        if (coiot.getVersion() >= 2) {
+            {
+                try {
+                    // Try to device description using http request (FW 1.10+)
+                    String payload = api.getCoIoTDescription();
+                    if (!payload.isEmpty()) {
+                        logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName);
+                        handleDeviceDescription(thingName, payload);
+                        return;
+                    }
+                } catch (ShellyApiException e) {
+                    // ignore if not supported by device
+                }
+            }
+        }
+        reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
+    }
+
+    /**
+     * Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
+     * JsonSyntaxException
+     *
+     * @param json to be checked/fixed
+     */
+    private static String fixJSON(String payload) {
+        String json = payload;
+        json = json.replace("}{", "},{");
+        json = json.replace("][", "],[");
+        json = json.replace("],,[", "],[");
+        return json;
+    }
+
+    /**
+     * Send a new request (Discovery to get Device Description). Before a pending
+     * request will be canceled.
+     *
+     * @param request The current request (this will be canceled an a new one will
+     *            be created)
+     * @param ipAddress Device's IP address
+     * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
+     * @param con true: send as CON, false: send as NON
+     * @return new packet
+     */
+    private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
+        if ((request != null) && !request.isCanceled()) {
+            request.cancel();
+        }
+
+        resetSerial();
+        return newRequest(ipAddress, coiotPort, uri, con).send();
+    }
+
+    /**
+     * Allocate a new Request structure. A message observer will be added to get the
+     * callback when a response has been received.
+     *
+     * @param ipAddress IP address of the device
+     * @param uri URI to be addressed
+     * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
+     * @param con true: send as CON, false: send as NON
+     * @return new packet
+     */
+
+    private Request newRequest(String ipAddress, int port, String uri, Type con) {
+        // We need to build our own Request to set an empty Token
+        Request request = new Request(Code.GET, con);
+        request.setURI(completeUrl(ipAddress, port, uri));
+        request.setToken(EMPTY_BYTE);
+        request.addMessageObserver(new MessageObserverAdapter() {
+            @Override
+            public void onResponse(@Nullable Response response) {
+                processResponse(response);
+            }
+
+            @Override
+            public void onCancel() {
+                logger.debug("{}: CoAP Request was canceled", thingName);
+            }
+
+            @Override
+            public void onTimeout() {
+                logger.debug("{}: CoAP Request timed out", thingName);
+            }
+        });
+        return request;
+    }
+
+    /**
+     * Reset serial and payload used to detect duplicate messages, which have to be ignored.
+     * We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
+     * sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
+     * the payload.
+     */
+    private void resetSerial() {
+        lastSerial = -1;
+        lastPayload = "";
+    }
+
+    public int getVersion() {
+        return coiotVers;
+    }
+
+    /**
+     * Cancel pending requests and shutdown the client
+     */
+    public synchronized void stop() {
+        if (isStarted()) {
+            logger.debug("{}: Stopping CoAP Listener", thingName);
+            coapServer.stop(this);
+            CoapClient cclient = statusClient;
+            if (cclient != null) {
+                cclient.shutdown();
+                statusClient = null;
+            }
+            Request request = reqDescription;
+            if (!request.isCanceled()) {
+                request.cancel();
+            }
+            request = reqStatus;
+            if (!request.isCanceled()) {
+                request.cancel();
+            }
+        }
+        resetSerial();
+        coiotBound = false;
+    }
+
+    public long getMessageCount() {
+        return coiotMessages;
+    }
+
+    public long getErrorCount() {
+        return coiotErrors;
+    }
+
+    public void dispose() {
+        stop();
+    }
+
+    private static String completeUrl(String ipAddress, int port, String uri) {
+        return "coap://" + ipAddress + ":" + port + uri;
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapJSonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapJSonDTO.java
new file mode 100644 (file)
index 0000000..3529bbb
--- /dev/null
@@ -0,0 +1,339 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * The {@link Shelly1CoapJSonDTO} helps the CoIoT Json into Java objects
+ *
+ * @author Markus Michels - Initial contribution
+ */
+public class Shelly1CoapJSonDTO {
+    // Coap
+    public static final int COIOT_VERSION_1 = 1;
+    public static final int COIOT_VERSION_2 = 2;
+
+    public static final int COIOT_PORT = 5683;
+    public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187";
+
+    public static final String COLOIT_URI_BASE = "/cit/";
+    public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d";
+    public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s";
+
+    public static final int COIOT_OPTION_GLOBAL_DEVID = 3332;
+    public static final int COIOT_OPTION_STATUS_VALIDITY = 3412;
+    public static final int COIOT_OPTION_STATUS_SERIAL = 3420;
+
+    public static final String COIOT_TAG_BLK = "blk";
+    public static final String COIOT_TAG_SEN = "sen";
+    public static final String COIOT_TAG_ACT = "act";
+    public static final String COIOT_TAG_GENERIC = "G";
+
+    public static class CoIotDescrBlk {
+        @SerializedName("I")
+        String id; // ID
+        @SerializedName("D")
+        String desc; // Description
+
+        // Sometimes sen entries are part of the blk array - not conforming the Spec!
+        @SerializedName("T")
+        public String type; // Type
+        @SerializedName("R")
+        public String range; // Range
+        @SerializedName("L")
+        public String links; // Links
+    }
+
+    public static class CoIotDescrSen {
+        @SerializedName("I")
+        String id; // ID
+        @SerializedName("D")
+        String desc = ""; // Description
+        @SerializedName("T")
+        public String type; // Type
+        @SerializedName("R")
+        public String range; // Range
+        @SerializedName("L")
+        public String links; // Links
+        @SerializedName("U")
+        public String unit; // Unit
+    }
+
+    public static class CoIotDescrP {
+        @SerializedName("I")
+        String id; // ID
+        @SerializedName("D")
+        String desc; // Description
+        @SerializedName("R")
+        public String range; // Range
+    }
+
+    public static class CoIotDescrAct {
+        @SerializedName("I")
+        String id; // ID
+        @SerializedName("D")
+        String desc; // Description
+        @SerializedName("L")
+        public String links; // Links
+        @SerializedName("P")
+        public List<CoIotDescrP> pTag; // ?
+    }
+
+    public static class CoIotDevDescription {
+        public List<CoIotDescrBlk> blk;
+        public List<CoIotDescrSen> sen;
+        // public List<CoIotDescrAct> act;
+
+        public CoIotDevDescription() {
+            blk = new ArrayList<>();
+            sen = new ArrayList<>();
+        }
+    }
+
+    public static class CoIotSensor {
+        @SerializedName("index")
+        public String id; // id
+        public double value; // value
+        public String valueStr; // value
+        public List<Object> valueArray;
+    }
+
+    public static class CoIotGenericSensorList {
+        @SerializedName("G")
+        public List<CoIotSensor> generic;
+
+        public CoIotGenericSensorList() {
+            generic = new ArrayList<>();
+        }
+    }
+
+    protected static class CoIotDevDescrTypeAdapter extends TypeAdapter<CoIotDevDescription> {
+        @Override
+        public CoIotDevDescription read(final JsonReader in) throws IOException {
+            CoIotDevDescription descr = new CoIotDevDescription();
+
+            /*
+             * parse JSON like
+             * "blk": [
+             * { "I": 0, "D": "Relay0"},
+             * { "I": 1, "D": "Sensors"} ],
+             * "sen": [
+             * { "I": 111, "T": "P", "D": "Power","R": "0/3500","L": 0},
+             * { "I": 112,"T": "S","D": "Switch","R": "0/1","L": 0}
+             * ]
+             */
+            in.beginObject();
+            String name = in.nextName();
+            if (name.equalsIgnoreCase(COIOT_TAG_BLK)) {
+                in.beginArray();
+                while (in.hasNext()) {
+                    CoIotDescrBlk blk = new CoIotDescrBlk();
+                    in.beginObject();
+                    while (in.hasNext()) {
+                        switch (in.nextName().toUpperCase()) {
+                            case "I":
+                                blk.id = in.nextString();
+                                break;
+                            case "D":
+                                blk.desc = in.nextString();
+                                break;
+                            default:
+                                // skip data
+                                in.nextNull();
+                        }
+                    }
+                    in.endObject();
+                    descr.blk.add(blk);
+                }
+                in.endArray();
+                name = in.nextName();
+            }
+
+            if (name.equalsIgnoreCase(COIOT_TAG_SEN)) {
+                /*
+                 * parse sensor list, e.g.
+                 * "sen":[
+                 * { "I":111,"T":"Red","R":"0/255","L":0},
+                 * { "I":121,"T":"Green","R":"0/255","L":0},
+                 * ]
+                 */
+                in.beginArray();
+                while (in.hasNext()) {
+                    CoIotDescrSen sen = new CoIotDescrSen();
+                    in.beginObject();
+                    while (in.hasNext()) {
+                        String tag = in.nextName();
+                        switch (tag.toUpperCase()) {
+                            case "I":
+                                sen.id = in.nextString();
+                                break;
+                            case "D":
+                                sen.desc = in.nextString();
+                                break;
+                            case "T":
+                                sen.type = in.nextString();
+                                break;
+                            case "R":
+                                JsonToken token = in.peek();
+                                if (token == JsonToken.BEGIN_ARRAY) {
+                                    // must be v2: an array
+                                    in.beginArray();
+                                    sen.range = "";
+                                    while (in.hasNext()) {
+                                        String value = in.nextString();
+                                        sen.range += sen.range.isEmpty() ? value : ";" + value;
+                                    }
+                                    in.endArray();
+                                } else {
+                                    sen.range = in.nextString();
+                                }
+                                break;
+                            case "L":
+                                sen.links = String.valueOf(in.nextInt());
+                                break;
+                            case "U": // New in CoAPv2: unit"
+                                sen.unit = in.nextString();
+                                break;
+                            default:
+                                // skip data
+                                in.nextNull();
+                        }
+                    }
+                    in.endObject();
+                    descr.sen.add(sen);
+                }
+
+                in.endArray();
+                if (in.hasNext()) {
+                    name = in.nextName();
+                }
+            }
+
+            if (name.equalsIgnoreCase(COIOT_TAG_ACT)) {
+                // skip record
+                in.skipValue();
+            }
+
+            in.endObject();
+            return descr;
+        }
+
+        @Override
+        public void write(final JsonWriter out, final CoIotDevDescription descr) throws IOException {
+            out.beginObject();
+            if (descr != null) {
+                out.name(COIOT_TAG_BLK).beginArray();
+                for (int i = 0; i < descr.blk.size(); i++) {
+                    CoIotDescrBlk blk = descr.blk.get(i);
+                    out.beginArray();
+                    out.value(blk.id);
+                    out.value(blk.desc);
+                    out.endArray();
+                }
+                out.endArray();
+
+                out.name(COIOT_TAG_SEN).beginArray();
+                for (int i = 0; i < descr.sen.size(); i++) {
+                    // Create element, e.g. {“I”:66, “D”:“lux”, “T”:“L”, “R”:“0/100000”, “L”:1},
+                    CoIotDescrSen sen = descr.sen.get(i);
+                    out.beginArray();
+                    out.value(sen.id);
+                    out.value(sen.desc);
+                    out.value(sen.type);
+                    out.value(sen.range);
+                    out.value(sen.links);
+                    if (sen.unit != null) {
+                        out.value(sen.unit);
+                    }
+                    out.endArray();
+                }
+                out.endArray();
+            }
+            out.endObject();
+        }
+    }
+
+    protected static class CoIotSensorTypeAdapter extends TypeAdapter<CoIotGenericSensorList> {
+        @Override
+        public CoIotGenericSensorList read(final JsonReader in) throws IOException {
+            CoIotGenericSensorList list = new CoIotGenericSensorList();
+
+            in.beginObject();
+            String generic = in.nextName();
+            if (generic.equals(COIOT_TAG_GENERIC)) {
+                in.beginArray();
+                while (in.hasNext()) {
+                    CoIotSensor sensor = new CoIotSensor();
+                    in.beginArray();
+                    in.nextInt(); // alway 0
+                    sensor.id = Integer.toString(in.nextInt());
+                    JsonToken token = in.peek();
+                    if (token == JsonToken.STRING) {
+                        // handle as string
+                        sensor.valueStr = in.nextString();
+                        sensor.value = -1;
+                    } else if (token == JsonToken.NUMBER) {
+                        // handle as double
+                        sensor.value = in.nextDouble();
+                        sensor.valueStr = "";
+                    } else if (token == JsonToken.BEGIN_ARRAY) {
+                        sensor.valueArray = new ArrayList<>();
+                        in.beginArray();
+                        while (in.hasNext()) {
+                            if (in.peek() == JsonToken.STRING) {
+                                sensor.valueArray.add(in.nextString());
+                            } else {
+                                // skip
+                                in.nextNull();
+                            }
+                        }
+                        in.endArray();
+                    }
+                    in.endArray();
+                    list.generic.add(sensor);
+                }
+                in.endArray();
+            }
+            in.endObject();
+
+            return list;
+        }
+
+        @Override
+        public void write(final JsonWriter out, final CoIotGenericSensorList o) throws IOException {
+            CoIotGenericSensorList sensors = o;
+            out.beginObject();
+            if (sensors != null) {
+                out.name(COIOT_TAG_GENERIC).beginArray();
+                for (int i = 0; i < sensors.generic.size(); i++) {
+                    out.beginArray();
+                    out.value(0);
+                    out.value(sensors.generic.get(i).id);
+                    out.value(sensors.generic.get(i).value);
+                    out.endArray();
+                }
+                out.endArray();
+            }
+            out.endObject();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapListener.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapListener.java
new file mode 100644 (file)
index 0000000..2b71b58
--- /dev/null
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link Shelly1CoapListener} describes the listening interface to process Coap responses
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public interface Shelly1CoapListener {
+    public void processResponse(@Nullable Response response);
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapServer.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1CoapServer.java
new file mode 100644 (file)
index 0000000..527aa24
--- /dev/null
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO.COIOT_PORT;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.coap.CoAP.Code;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.CoapEndpoint;
+import org.eclipse.californium.core.network.Exchange;
+import org.eclipse.californium.core.network.config.NetworkConfig;
+import org.eclipse.californium.elements.UdpMulticastConnector;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Shelly1CoapServer} implements the UDP listener and status event processor (for /cit/s messages)
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1CoapServer {
+    private final Logger logger = LoggerFactory.getLogger(Shelly1CoapServer.class);
+
+    boolean started = false;
+    private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
+    private @Nullable UdpMulticastConnector statusConnector;
+    private CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);
+    private final Set<Shelly1CoapListener> coapListeners = ConcurrentHashMap.newKeySet();
+
+    protected class ShellyStatusListener extends CoapResource {
+        private Shelly1CoapServer listener;
+
+        public ShellyStatusListener(String uri, Shelly1CoapServer listener) {
+            super(uri, true);
+            getAttributes().setTitle("ShellyCoapListener");
+            this.listener = listener;
+        }
+
+        @Override
+        public void handleRequest(@Nullable final Exchange exchange) {
+            if (exchange != null) {
+                Request request = exchange.getRequest();
+                Code code = exchange.getRequest().getCode();
+                switch (code) {
+                    case CUSTOM_30:
+                    case PUT: // Shelly Motion beta: incorrect, but handle the format
+                        listener.processResponse(createResponse(request));
+                        break;
+                    default:
+                        super.handleRequest(exchange);
+                }
+            }
+        }
+    }
+
+    public synchronized void start(String localIp, int port, Shelly1CoapListener listener)
+            throws UnknownHostException, SocketException {
+        if (!started) {
+            logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, port);
+            NetworkConfig nc = NetworkConfig.getStandard();
+            InetAddress localAddr = InetAddress.getByName(localIp);
+            InetSocketAddress localPort = new InetSocketAddress(port);
+
+            // Join the multicast group on the selected network interface
+            statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
+            statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
+            server = new CoapServer(NetworkConfig.getStandard(), port);
+            server.addEndpoint(statusEndpoint);
+            CoapResource cit = new ShellyStatusListener("cit", this);
+            CoapResource s = new ShellyStatusListener("s", this);
+            cit.add(s);
+            server.add(cit);
+            started = true;
+        }
+
+        if (!coapListeners.contains(listener)) {
+            coapListeners.add(listener);
+        }
+    }
+
+    protected void processResponse(Response response) {
+        coapListeners.forEach(listener -> listener.processResponse(response));
+    }
+
+    public static Response createResponse(Request request) {
+        Response response = Response.createResponse(request, ResponseCode.CONTENT);
+        response.setType(request.getType());
+        response.setSourceContext(request.getSourceContext());
+        response.setMID(request.getMID());
+        response.setOptions(request.getOptions());
+        response.setPayload(request.getPayload());
+        return response;
+    }
+
+    @Nullable
+    public CoapEndpoint getEndpoint() {
+        return statusEndpoint;
+    }
+
+    /**
+     * Cancel pending requests and shutdown the client
+     */
+    public void stop(Shelly1CoapListener listener) {
+        coapListeners.remove(listener);
+        if (coapListeners.isEmpty()) {
+            stop();
+        }
+    }
+
+    private synchronized void stop() {
+        if (started) {
+            // Last listener
+            server.stop();
+            statusEndpoint.stop();
+            coapListeners.clear();
+            started = false;
+            logger.debug("CoAP Listener stopped");
+        }
+    }
+
+    public void dispose() {
+        stop();
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/api1/Shelly1HttpApi.java
new file mode 100644 (file)
index 0000000..9c6b8b7
--- /dev/null
@@ -0,0 +1,714 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.shelly.internal.api1;
+
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.shelly.internal.api.ShellyApiException;
+import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
+import org.openhab.binding.shelly.internal.api.ShellyApiResult;
+import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySendKeyList;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySenseKeyCode;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
+import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
+import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
+import org.openhab.core.library.unit.ImperialUnits;
+import org.openhab.core.library.unit.SIUnits;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * {@link Shelly1HttpApi} wraps the Shelly REST API and provides various low level function to access the device api
+ * (not
+ * cloud api).
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+public class Shelly1HttpApi extends ShellyHttpClient implements ShellyApiInterface {
+    private final Logger logger = LoggerFactory.getLogger(Shelly1HttpApi.class);
+    private final ShellyDeviceProfile profile;
+
+    public Shelly1HttpApi(String thingName, ShellyThingInterface thing) {
+        super(thingName, thing);
+        profile = thing.getProfile();
+    }
+
+    /**
+     * Simple initialization - called by discovery handler
+     *
+     * @param thingName Symbolic thing name
+     * @param config Thing Configuration
+     * @param httpClient HTTP Client to be passed to ShellyHttpClient
+     */
+    public Shelly1HttpApi(String thingName, ShellyThingConfiguration config, HttpClient httpClient) {
+        super(thingName, config, httpClient);
+        this.profile = new ShellyDeviceProfile();
+    }
+
+    @Override
+    public ShellySettingsDevice getDeviceInfo() throws ShellyApiException {
+        return callApi(SHELLY_URL_DEVINFO, ShellySettingsDevice.class);
+    }
+
+    @Override
+    public String setDebug(boolean enabled) throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "?debug_enable=" + Boolean.valueOf(enabled), String.class);
+    }
+
+    @Override
+    public String getDebugLog(String id) throws ShellyApiException {
+        return callApi("/debug/" + id, String.class);
+    }
+
+    /**
+     * Initialize the device profile
+     *
+     * @param thingType Type of DEVICE as returned from the thing properties (based on discovery)
+     * @return Initialized ShellyDeviceProfile
+     * @throws ShellyApiException
+     */
+    @Override
+    public ShellyDeviceProfile getDeviceProfile(String thingType) throws ShellyApiException {
+        String json = httpRequest(SHELLY_URL_SETTINGS);
+        if (json.contains("\"type\":\"SHDM-")) {
+            logger.trace("{}: Detected a Shelly Dimmer: fix Json (replace lights[] tag with dimmers[]", thingName);
+            json = fixDimmerJson(json);
+        }
+
+        // Map settings to device profile for Light and Sense
+        profile.initialize(thingType, json);
+
+        // 2nd level initialization
+        profile.thingName = profile.hostname;
+        if (profile.isLight && (profile.numMeters == 0)) {
+            logger.debug("{}: Get number of meters from light status", thingName);
+            ShellyStatusLight status = getLightStatus();
+            profile.numMeters = status.meters != null ? status.meters.size() : 0;
+        }
+        if (profile.isSense) {
+            profile.irCodes = getIRCodeList();
+            logger.debug("{}: Sense stored key list loaded, {} entries.", thingName, profile.irCodes.size());
+        }
+
+        return profile;
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return profile.initialized;
+    }
+
+    /**
+     * Get generic device settings/status. Json returned from API will be mapped to a Gson object
+     *
+     * @return Device settings/status as ShellySettingsStatus object
+     * @throws ShellyApiException
+     */
+    @Override
+    public ShellySettingsStatus getStatus() throws ShellyApiException {
+        String json = "";
+        try {
+            json = httpRequest(SHELLY_URL_STATUS);
+            // Dimmer2 returns invalid json type for loaderror :-(
+            json = getString(json.replace("\"loaderror\":0,", "\"loaderror\":false,"));
+            json = getString(json.replace("\"loaderror\":1,", "\"loaderror\":true,"));
+            ShellySettingsStatus status = fromJson(gson, json, ShellySettingsStatus.class);
+            status.json = json;
+            return status;
+        } catch (JsonSyntaxException e) {
+            throw new ShellyApiException("Unable to parse JSON: " + json, e);
+        }
+    }
+
+    @Override
+    public ShellyStatusRelay getRelayStatus(int relayIndex) throws ShellyApiException {
+        return callApi(SHELLY_URL_STATUS_RELEAY + "/" + relayIndex, ShellyStatusRelay.class);
+    }
+
+    @Override
+    public void setRelayTurn(int id, String turnMode) throws ShellyApiException {
+        callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
+                ShellyShortLightStatus.class);
+    }
+
+    @Override
+    public ShellyShortLightStatus setLightTurn(int id, String turnMode) throws ShellyApiException {
+        return callApi(getControlUriPrefix(id) + "?" + SHELLY_LIGHT_TURN + "=" + turnMode.toLowerCase(),
+                ShellyShortLightStatus.class);
+    }
+
+    @Override
+    public void setBrightness(int id, int brightness, boolean autoOn) throws ShellyApiException {
+        String turn = autoOn ? SHELLY_LIGHT_TURN + "=" + SHELLY_API_ON + "&" : "";
+        httpRequest(getControlUriPrefix(id) + "?" + turn + "brightness=" + brightness);
+    }
+
+    @Override
+    public ShellyRollerStatus getRollerStatus(int rollerIndex) throws ShellyApiException {
+        String uri = SHELLY_URL_CONTROL_ROLLER + "/" + rollerIndex + "/pos";
+        return callApi(uri, ShellyRollerStatus.class);
+    }
+
+    @Override
+    public void setRollerTurn(int relayIndex, String turnMode) throws ShellyApiException {
+        httpRequest(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex + "?go=" + turnMode);
+    }
+
+    @Override
+    public void setRollerPos(int relayIndex, int position) throws ShellyApiException {
+        httpRequest(SHELLY_URL_CONTROL_ROLLER + "/" + relayIndex + "?go=to_pos&roller_pos=" + position);
+    }
+
+    @Override
+    public ShellyShortLightStatus getLightStatus(int index) throws ShellyApiException {
+        return callApi(getControlUriPrefix(index), ShellyShortLightStatus.class);
+    }
+
+    @Override
+    public ShellyStatusSensor getSensorStatus() throws ShellyApiException {
+        ShellyStatusSensor status = callApi(SHELLY_URL_STATUS, ShellyStatusSensor.class);
+        if (profile.isSense) {
+            // complete reported data, map C to F or vice versa: C=(F - 32) * 0.5556;
+            status.tmp.tC = status.tmp.units.equals(SHELLY_TEMP_CELSIUS) ? status.tmp.value
+                    : ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(status.tmp.value))
+                            .doubleValue();
+            double f = (double) SIUnits.CELSIUS.getConverterTo(ImperialUnits.FAHRENHEIT)
+                    .convert(getDouble(status.tmp.value));
+            status.tmp.tF = status.tmp.units.equals(SHELLY_TEMP_FAHRENHEIT) ? status.tmp.value : f;
+        }
+        if ((status.charger == null) && (profile.settings.externalPower != null)) {
+            // SHelly H&T uses external_power, Sense uses charger
+            status.charger = profile.settings.externalPower != 0;
+        }
+        return status;
+    }
+
+    @Override
+    public void setTimer(int index, String timerName, int value) throws ShellyApiException {
+        String type = SHELLY_CLASS_RELAY;
+        if (profile.isRoller) {
+            type = SHELLY_CLASS_ROLLER;
+        } else if (profile.isLight) {
+            type = SHELLY_CLASS_LIGHT;
+        }
+        String uri = SHELLY_URL_SETTINGS + "/" + type + "/" + index + "?" + timerName + "=" + value;
+        httpRequest(uri);
+    }
+
+    @Override
+    public void setSleepTime(int value) throws ShellyApiException {
+        httpRequest(SHELLY_URL_SETTINGS + "?sleep_time=" + value);
+    }
+
+    @Override
+    public void setValveTemperature(int valveId, int value) throws ShellyApiException {
+        request("/thermostat/" + valveId + "?target_t_enabled=1&target_t=" + value);
+    }
+
+    @Override
+    public void setValveMode(int valveId, boolean auto) throws ShellyApiException {
+        String uri = "/settings/thermostat/" + valveId + "?target_t_enabled=" + (auto ? "1" : "0");
+        if (auto) {
+            uri = uri + "&target_t=" + getDouble(profile.settings.thermostats.get(0).targetTemp.value);
+        }
+        httpRequest(uri); // percentage to open the valve
+    }
+
+    @Override
+    public void setValveProfile(int valveId, int value) throws ShellyApiException {
+        String uri = "/settings/thermostat/" + valveId + "?";
+        request(uri + (value == 0 ? "schedule=0" : "schedule=1&schedule_profile=" + value));
+    }
+
+    @Override
+    public void setValvePosition(int valveId, double value) throws ShellyApiException {
+        request("/thermostat/" + valveId + "?pos=" + value); // percentage to open the valve
+    }
+
+    @Override
+    public void setValveBoostTime(int valveId, int value) throws ShellyApiException {
+        request("/settings/thermostat/" + valveId + "?boost_minutes=" + value);
+    }
+
+    @Override
+    public void startValveBoost(int valveId, int value) throws ShellyApiException {
+        int minutes = value != -1 ? value : getInteger(profile.settings.thermostats.get(0).boostMinutes);
+        httpRequest("/thermostat/0?boost_minutes=" + minutes);
+    }
+
+    @Override
+    public void setLedStatus(String ledName, Boolean value) throws ShellyApiException {
+        httpRequest(SHELLY_URL_SETTINGS + "?" + ledName + "=" + (value ? SHELLY_API_TRUE : SHELLY_API_FALSE));
+    }
+
+    public ShellySettingsLight getLightSettings() throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS_LIGHT, ShellySettingsLight.class);
+    }
+
+    @Override
+    public ShellyStatusLight getLightStatus() throws ShellyApiException {
+        return callApi(SHELLY_URL_STATUS, ShellyStatusLight.class);
+    }
+
+    public void setLightSetting(String parm, String value) throws ShellyApiException {
+        httpRequest(SHELLY_URL_SETTINGS + "?" + parm + "=" + value);
+    }
+
+    @Override
+    public ShellySettingsLogin getLoginSettings() throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "/login", ShellySettingsLogin.class);
+    }
+
+    @Override
+    public ShellySettingsLogin setLoginCredentials(String user, String password) throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "/login?enabled=yes&username=" + urlEncode(user) + "&password="
+                + urlEncode(password), ShellySettingsLogin.class);
+    }
+
+    @Override
+    public String getCoIoTDescription() throws ShellyApiException {
+        try {
+            return callApi("/cit/d", String.class);
+        } catch (ShellyApiException e) {
+            if (e.getApiResult().isNotFound()) {
+                return ""; // only supported by FW 1.10+
+            }
+            throw e;
+        }
+    }
+
+    @Override
+    public ShellySettingsLogin setCoIoTPeer(String peer) throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "?coiot_enable=true&coiot_peer=" + peer, ShellySettingsLogin.class);
+    }
+
+    @Override
+    public String deviceReboot() throws ShellyApiException {
+        return callApi(SHELLY_URL_RESTART, String.class);
+    }
+
+    @Override
+    public String factoryReset() throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "?reset=true", String.class);
+    }
+
+    @Override
+    public ShellyOtaCheckResult checkForUpdate() throws ShellyApiException {
+        return callApi("/ota/check", ShellyOtaCheckResult.class); // nw FW 1.10+: trigger update check
+    }
+
+    @Override
+    public String setWiFiRecovery(boolean enable) throws ShellyApiException {
+        return callApi(SHELLY_URL_SETTINGS + "?wifirecovery_reboot_enabled=" + (enable ? "true" : "false"),
+                String.class); // FW 1.10+: Enable auto-restart on WiFi problems
+    }
+
+    @Override
+    public String setApRoaming(boolean enable) throws ShellyApiException { // FW 1.10+: Enable AP Roadming
+        return callApi(SHELLY_URL_SETTINGS + "?ap_roaming_enabled=" + (enable ? "true" : "false"), String.class);
+    }
+
+    @Override
+    public String resetStaCache() throws ShellyApiException { // FW 1.10+: Reset cached STA/AP list and to a rescan
+        return callApi("/sta_cache_reset", String.class);
+    }
+
+    public ShellySettingsUpdate firmwareUpdate(String uri) throws ShellyApiException {
+        return callApi("/ota?" + uri, ShellySettingsUpdate.class);
+    }
+
+    @Override
+    public String setCloud(boolean enabled) throws ShellyApiException {
+        return callApi("/settings/cloud/?enabled=" + (enabled ? "1" : "0"), String.class);
+    }
+
+    /**
+     * Change between White and Color Mode
+     *
+     * @param mode
+     * @throws ShellyApiException
+     */
+    @Override
+    public void setLightMode(String mode) throws ShellyApiException {
+        if (!mode.isEmpty() && !profile.mode.equals(mode)) {
+            setLightSetting(SHELLY_API_MODE, mode);
+            profile.mode = mode;
+            profile.inColor = profile.isLight && profile.mode.equalsIgnoreCase(SHELLY_MODE_COLOR);
+        }
+    }
+
+    /**
+     * Set a single light parameter
+     *
+     * @param lightIndex Index of the light, usually 0 for Bulb and 0..3 for RGBW2.
+     * @param parm Name of the parameter (see API spec)
+     * @param value The value
+     * @throws ShellyApiException
+     */
+    @Override
+    public void setLightParm(int lightIndex, String parm, String value) throws ShellyApiException {
+        // Bulb, RGW2: /<color mode>/<light id>?parm?value
+        // Dimmer: /light/<light id>?parm=value
+        httpRequest(getControlUriPrefix(lightIndex) + "?" + parm + "=" + value);
+    }
+
+    @Override
+    public void setLightParms(int lightIndex, Map<String, String> parameters) throws ShellyApiException {
+        String url = getControlUriPrefix(lightIndex) + "?";
+        int i = 0;
+        for (String key : parameters.keySet()) {
+            if (i > 0) {
+                url = url + "&";
+            }
+            url = url + key + "=" + parameters.get(key);
+            i++;
+        }
+        httpRequest(url);
+    }
+
+    /**
+     * Retrieve the IR Code list from the Shelly Sense device. The list could be customized by the user. It defines the
+     * symbolic key code, which gets
+     * map into a PRONTO code
+     *
+     * @return Map of key codes
+     * @throws ShellyApiException
+     */
+    public Map<String, String> getIRCodeList() throws ShellyApiException {
+        String result = httpRequest(SHELLY_URL_LIST_IR);
+        // take pragmatic approach to make the returned JSon into named arrays for Gson parsing
+        String keyList = substringAfter(result, "[");
+        keyList = substringBeforeLast(keyList, "]");
+        keyList = keyList.replaceAll(java.util.regex.Pattern.quote("\",\""), "\", \"name\": \"");
+        keyList = keyList.replaceAll(java.util.regex.Pattern.quote("["), "{ \"id\":");
+        keyList = keyList.replaceAll(java.util.regex.Pattern.quote("]"), "} ");
+        String json = "{\"key_codes\" : [" + keyList + "] }";
+        ShellySendKeyList codes = fromJson(gson, json, ShellySendKeyList.class);
+        Map<String, String> list = new HashMap<>();
+        for (ShellySenseKeyCode key : codes.keyCodes) {
+            if (key != null) {
+                list.put(key.id, key.name);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * Sends a IR key code to the Shelly Sense.
+     *
+     * @param keyCode A keyCoud could be a symbolic name (as defined in the key map on the device) or a PRONTO Code in
+     *            plain or hex64 format
+     *
+     * @throws ShellyApiException
+     * @throws IllegalArgumentException
+     */
+    @Override
+    public void sendIRKey(String keyCode) throws ShellyApiException, IllegalArgumentException {
+        String type = "";
+        if (profile.irCodes.containsKey(keyCode)) {
+            type = SHELLY_IR_CODET_STORED;
+        } else if ((keyCode.length() > 4) && keyCode.contains(" ")) {
+            type = SHELLY_IR_CODET_PRONTO;
+        } else {
+            type = SHELLY_IR_CODET_PRONTO_HEX;
+        }
+        String url = SHELLY_URL_SEND_IR + "?type=" + type;
+        if (type.equals(SHELLY_IR_CODET_STORED)) {
+            url = url + "&" + "id=" + keyCode;
+        } else if (type.equals(SHELLY_IR_CODET_PRONTO)) {
+            String code = Base64.getEncoder().encodeToString(keyCode.getBytes(StandardCharsets.UTF_8));
+            url = url + "&" + SHELLY_IR_CODET_PRONTO + "=" + code;
+        } else if (type.equals(SHELLY_IR_CODET_PRONTO_HEX)) {
+            url = url + "&" + SHELLY_IR_CODET_PRONTO_HEX + "=" + keyCode;
+        }
+        httpRequest(url);
+    }
+
+    public void setSenseSetting(String setting, String value) throws ShellyApiException {
+        httpRequest(SHELLY_URL_SETTINGS + "?" + setting + "=" + value);
+    }
+
+    /**
+     * Set event callback URLs. Depending on the device different event types are supported. In fact all of them will be
+     * redirected to the binding's servlet and act as a trigger to schedule a status update
+     *
+     * @param ShellyApiException
+     * @throws ShellyApiException
+     */
+    @Override
+    public void setActionURLs() throws ShellyApiException {
+        setRelayEvents();
+        setDimmerEvents();
+        setSensorEventUrls();
+    }
+
+    private void setRelayEvents() throws ShellyApiException {
+        if (profile.settings.relays != null) {
+            int num = profile.isRoller ? profile.numRollers : profile.numRelays;
+            for (int i = 0; i < num; i++) {
+                setEventUrls(i);
+            }
+        }
+    }
+
+    private void setDimmerEvents() throws ShellyApiException {
+        if (profile.settings.dimmers != null) {
+            int sz = profile.settings.dimmers.size();
+            for (int i = 0; i < sz; i++) {
+                setEventUrls(i);
+            }
+        } else if (profile.isLight) {
+            setEventUrls(0);
+        }
+    }
+
+    /**
+     * Set sensor Action URLs
+     *
+     * @throws ShellyApiException
+     */
+    private void setSensorEventUrls() throws ShellyApiException, ShellyApiException {
+        if (profile.isSensor) {
+            logger.debug("{}: Set Sensor Reporting URL", thingName);
+            setEventUrl(config.eventsSensorReport, SHELLY_EVENT_SENSORREPORT, SHELLY_EVENT_DARK, SHELLY_EVENT_TWILIGHT,
+                    SHELLY_EVENT_FLOOD_DETECTED, SHELLY_EVENT_FLOOD_GONE, SHELLY_EVENT_OPEN, SHELLY_EVENT_CLOSE,
+                    SHELLY_EVENT_VIBRATION, SHELLY_EVENT_ALARM_MILD, SHELLY_EVENT_ALARM_HEAVY, SHELLY_EVENT_ALARM_OFF,
+                    SHELLY_EVENT_TEMP_OVER, SHELLY_EVENT_TEMP_UNDER);
+        }
+    }
+
+    /**
+     * Set/delete Relay/Roller/Dimmer Action URLs
+     *
+     * @param index Device Index (0-based)
+     * @throws ShellyApiException
+     */
+    private void setEventUrls(Integer index) throws ShellyApiException {
+        if (profile.isRoller) {
+            setEventUrl(EVENT_TYPE_ROLLER, 0, config.eventsRoller, SHELLY_EVENT_ROLLER_OPEN, SHELLY_EVENT_ROLLER_CLOSE,
+                    SHELLY_EVENT_ROLLER_STOP);
+        } else if (profile.isDimmer) {
+            // 2 set of URLs
+            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsButton, SHELLY_EVENT_BTN1_ON, SHELLY_EVENT_BTN1_OFF,
+                    SHELLY_EVENT_BTN2_ON, SHELLY_EVENT_BTN2_OFF);
+            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH1, SHELLY_EVENT_LONGPUSH1,
+                    SHELLY_EVENT_SHORTPUSH2, SHELLY_EVENT_LONGPUSH2);
+
+            // Relay output
+            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
+        } else if (profile.hasRelays) {
+            // Standard relays: btn_xxx, out_xxx, short/longpush URLs
+            setEventUrl(EVENT_TYPE_RELAY, index, config.eventsButton, SHELLY_EVENT_BTN_ON, SHELLY_EVENT_BTN_OFF);
+            setEventUrl(EVENT_TYPE_RELAY, index, config.eventsPush, SHELLY_EVENT_SHORTPUSH, SHELLY_EVENT_LONGPUSH);
+            setEventUrl(EVENT_TYPE_RELAY, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
+        } else if (profile.isLight) {
+            // Duo, Bulb
+            setEventUrl(EVENT_TYPE_LIGHT, index, config.eventsSwitch, SHELLY_EVENT_OUT_ON, SHELLY_EVENT_OUT_OFF);
+        }
+    }
+
+    private void setEventUrl(boolean enabled, String... eventTypes) throws ShellyApiException {
+        if (config.localIp.isEmpty()) {
+            throw new ShellyApiException(thingName + ": Local IP address was not detected, can't build Callback URL");
+        }
+        for (String eventType : eventTypes) {
+            if (profile.containsEventUrl(eventType)) {
+                // H&T adds the type=xx to report_url itself, so we need to ommit here
+                String eclass = profile.isSensor ? EVENT_TYPE_SENSORDATA : eventType;
+                String urlParm = eventType.contains("temp") || profile.isHT ? "" : "?type=" + eventType;
+                String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+                        + profile.thingName + "/" + eclass + urlParm;
+                String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
+                String testUrl = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
+                if (!enabled && !profile.settingsJson.contains(testUrl)) {
+                    // Don't set URL to null when the current one doesn't point to this OH
+                    // Don't interfere with a 3rd party App
+                    continue;
+                }
+                if (!profile.settingsJson.contains(testUrl)) {
+                    // Current Action URL is != new URL
+                    logger.debug("{}: Set new url for event type {}: {}", thingName, eventType, newUrl);
+                    httpRequest(SHELLY_URL_SETTINGS + "?" + mkEventUrl(eventType) + "=" + urlEncode(newUrl));
+                }
+            }
+        }
+    }
+
+    private void setEventUrl(String deviceClass, Integer index, boolean enabled, String... eventTypes)
+            throws ShellyApiException {
+        for (String eventType : eventTypes) {
+            if (profile.containsEventUrl(eventType)) {
+                String callBackUrl = "http://" + config.localIp + ":" + config.localPort + SHELLY_CALLBACK_URI + "/"
+                        + profile.thingName + "/" + deviceClass + "/" + index + "?type=" + eventType;
+                String newUrl = enabled ? callBackUrl : SHELLY_NULL_URL;
+                String test = "\"" + mkEventUrl(eventType) + "\":\"" + callBackUrl + "\"";
+                if (!enabled && !profile.settingsJson.contains(test)) {
+                    // Don't set URL to null when the current one doesn't point to this OH
+                    // Don't interfere with a 3rd party App
+                    continue;
+                }
+                test = "\"" + mkEventUrl(eventType) + "\":\"" + newUrl + "\"";
+                if (!profile.settingsJson.contains(test)) {
+                    // Current Action URL is != new URL
+                    logger.debug("{}: Set URL for type {} to {}", thingName, eventType, newUrl);
+                    httpRequest(SHELLY_URL_SETTINGS + "/" + deviceClass + "/" + index + "?" + mkEventUrl(eventType)
+                            + "=" + urlEncode(newUrl));
+                }
+            }
+        }
+    }
+
+    private static String mkEventUrl(String eventType) {
+        return eventType + SHELLY_EVENTURL_SUFFIX;
+    }
+
+    /**
+     * Submit GET request and return response, check for invalid responses
+     *
+     * @param uri: URI (e.g. "/settings")
+     */
+    @Override
+    public <T> T callApi(String uri, Class<T> classOfT) throws ShellyApiException {
+        String json = request(uri);
+        return fromJson(gson, json, classOfT);
+    }
+
+    private String request(String uri) throws ShellyApiException {
+        ShellyApiResult apiResult = new ShellyApiResult();
+        int retries = 3;
+        boolean timeout = false;
+        while (retries > 0) {
+            try {
+                apiResult = innerRequest(HttpMethod.GET, uri);
+                if (timeout) {
+                    logger.debug("{}: API timeout #{}/{} recovered ({})", thingName, timeoutErrors, timeoutsRecovered,
+                            apiResult.getUrl());
+                    timeoutsRecovered++;
+                }
+                return apiResult.response; // successful
+            } catch (ShellyApiException e) {
+                if ((!e.isTimeout() && !apiResult.isHttpServerError()) || profile.hasBattery || (retries == 0)) {
+                    // Sensor in sleep mode or API exception for non-battery device or retry counter expired
+                    throw e; // non-timeout exception
+                }
+
+                timeout = true;
+                retries--;
+                timeoutErrors++; // count the retries
+                logger.debug("{}: API Timeout, retry #{} ({})", thingName, timeoutErrors, e.toString());
+            }
+        }
+        throw new ShellyApiException("API Timeout or inconsistent result"); // successful
+    }
+
+    private ShellyApiResult innerRequest(HttpMethod method, String uri) throws ShellyApiException {
+        Request request = null;
+        String url = "http://" + config.deviceIp + uri;
+        ShellyApiResult apiResult = new ShellyApiResult(method.toString(), url);
+
+        try {
+            request = httpClient.newRequest(url).method(method.toString()).timeout(SHELLY_API_TIMEOUT_MS,
+                    TimeUnit.MILLISECONDS);
+
+            if (!config.userId.isEmpty()) {
+                String value = config.userId + ":" + config.password;
+                request.header(HTTP_HEADER_AUTH,
+                        HTTP_AUTH_TYPE_BASIC + " " + Base64.getEncoder().encodeToString(value.getBytes()));
+            }
+            request.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON);
+            logger.trace("{}: HTTP {} for {}", thingName, method, url);
+
+            // Do request and get response
+            ContentResponse contentResponse = request.send();
+            apiResult = new ShellyApiResult(contentResponse);
+            String response = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
+            logger.trace("{}: HTTP Response {}: {}", thingName, contentResponse.getStatus(), response);
+
+            // validate response, API errors are reported as Json
+            if (contentResponse.getStatus() != HttpStatus.OK_200) {
+                throw new ShellyApiException(apiResult);
+            }
+            if (response.isEmpty() || !response.startsWith("{") && !response.startsWith("[") && !url.contains("/debug/")
+                    && !url.contains("/sta_cache_reset")) {
+                throw new ShellyApiException("Unexpected response: " + response);
+            }
+        } catch (ExecutionException | InterruptedException | TimeoutException | IllegalArgumentException e) {
+            ShellyApiException ex = new ShellyApiException(apiResult, e);
+            if (!ex.isTimeout()) { // will be handled by the caller
+                logger.trace("{}: API call returned exception", thingName, ex);
+            }
+            throw ex;
+        }
+        return apiResult;
+    }
+
+    @Override
+    public String getControlUriPrefix(Integer id) {
+        String uri = "";
+        if (profile.isLight || profile.isDimmer) {
+            if (profile.isDuo || profile.isDimmer) {
+                // Duo + Dimmer
+                uri = SHELLY_URL_CONTROL_LIGHT;
+            } else {
+                // Bulb + RGBW2
+                uri = "/" + (profile.inColor ? SHELLY_MODE_COLOR : SHELLY_MODE_WHITE);
+            }
+        } else {
+            // Roller, Relay
+            uri = SHELLY_URL_CONTROL_RELEAY;
+        }
+        uri = uri + "/" + id;
+        return uri;
+    }
+
+    @Override
+    public int getTimeoutErrors() {
+        return timeoutErrors;
+    }
+
+    @Override
+    public int getTimeoutsRecovered() {
+        return timeoutsRecovered;
+    }
+}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTInterface.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTInterface.java
deleted file mode 100644 (file)
index 158f50d..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.core.types.State;
-
-/**
- * The {@link ShellyCoapListener} describes the listening interface to process Coap responses
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public interface ShellyCoIoTInterface {
-    public int getVersion();
-
-    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap);
-
-    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap);
-
-    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
-            Map<String, State> updates, ShellyColorUtils col);
-
-    public String getLastWakeup();
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTProtocol.java
deleted file mode 100644 (file)
index ce51241..0000000
+++ /dev/null
@@ -1,400 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.OpenClosedType;
-import org.openhab.core.library.types.StringType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * The {@link ShellyCoIoTProtocol} implements common functions for the CoIoT implementations
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoIoTProtocol {
-    private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTProtocol.class);
-    protected final String thingName;
-    protected final ShellyThingInterface thingHandler;
-    protected final ShellyDeviceProfile profile;
-    protected final ShellyApiInterface api;
-    protected final Map<String, CoIotDescrBlk> blkMap;
-    protected final Map<String, CoIotDescrSen> sensorMap;
-    private final Gson gson = new GsonBuilder().create();
-
-    // Due to the fact that the device reports only the current/last status, but no real events, we need to distinguish
-    // between a real update or just a repeated status on periodic updates
-    protected int lastCfgCount = -1;
-    protected int[] lastEventCount = { -1, -1, -1, -1, -1, -1, -1, -1 }; // 4Pro has 4 relays, so 8 should be fine
-    protected String[] inputEvent = { "", "", "", "", "", "", "", "" };
-    protected String lastWakeup = "";
-
-    public ShellyCoIoTProtocol(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
-            Map<String, CoIotDescrSen> sensorMap) {
-        this.thingName = thingName;
-        this.thingHandler = thingHandler;
-        this.blkMap = blkMap;
-        this.sensorMap = sensorMap;
-        this.profile = thingHandler.getProfile();
-        this.api = thingHandler.getApi();
-    }
-
-    protected boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, CoIotSensor s,
-            Map<String, State> updates, ShellyColorUtils col) {
-        // Process status information and convert into channel updates
-        int rIndex = getIdFromBlk(sen);
-        String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
-                : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
-
-        switch (sen.type.toLowerCase()) {
-            case "b": // BatteryLevel +
-                updateChannel(updates, CHANNEL_GROUP_BATTERY, CHANNEL_SENSOR_BAT_LEVEL,
-                        toQuantityType(s.value, 0, Units.PERCENT));
-                break;
-            case "h" /* Humidity */:
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_HUM,
-                        toQuantityType(s.value, DIGITS_PERCENT, Units.PERCENT));
-                break;
-            case "m" /* Motion */:
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
-                        s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                break;
-            case "l": // Luminosity +
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_LUX,
-                        toQuantityType(s.value, DIGITS_LUX, Units.LUX));
-                break;
-            case "s": // CatchAll
-                switch (sen.desc.toLowerCase()) {
-                    case "state": // Relay status +
-                    case "output":
-                        updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
-                        break;
-                    case "input":
-                        handleInput(sen, s, rGroup, updates);
-                        break;
-                    case "brightness":
-                        // already handled by state/output
-                        break;
-                    case "overtemp": // ++
-                        if (s.value == 1) {
-                            thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
-                        }
-                        break;
-                    case "position":
-                        // work around: Roller reports 101% instead max 100
-                        double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
-                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
-                                toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
-                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
-                                toQuantityType(pos, Units.PERCENT));
-                        break;
-                    case "flood":
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
-                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                        break;
-                    case "vibration": // DW with FW1.6.5+
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
-                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                        if (s.value == 1) {
-                            thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
-                                    EVENT_TYPE_VIBRATION);
-                        }
-                        break;
-                    case "luminositylevel": // +
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ILLUM, getStringType(s.valueStr));
-                        break;
-                    case "charger": // Sense
-                        updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_CHARGER,
-                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                        break;
-                    // RGBW2/Bulb
-                    case "red":
-                        col.setRed((int) s.value);
-                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_RED,
-                                ShellyColorUtils.toPercent((int) s.value));
-                        break;
-                    case "green":
-                        col.setGreen((int) s.value);
-                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GREEN,
-                                ShellyColorUtils.toPercent((int) s.value));
-                        break;
-                    case "blue":
-                        col.setBlue((int) s.value);
-                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_BLUE,
-                                ShellyColorUtils.toPercent((int) s.value));
-                        break;
-                    case "white":
-                        col.setWhite((int) s.value);
-                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_WHITE,
-                                ShellyColorUtils.toPercent((int) s.value));
-                        break;
-                    case "gain":
-                        col.setGain((int) s.value);
-                        updateChannel(updates, CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_GAIN,
-                                ShellyColorUtils.toPercent((int) s.value, SHELLY_MIN_GAIN, SHELLY_MAX_GAIN));
-                        break;
-                    case "sensorerror":
-                        String sensorError = s.valueStr != null ? getString(s.valueStr) : "" + s.value;
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(sensorError));
-                        break;
-                    default:
-                        // Unknown
-                        return false;
-                }
-                break;
-
-            default:
-                // Unknown type
-                return false;
-        }
-
-        return true;
-    }
-
-    public static boolean updateChannel(Map<String, State> updates, String group, String channel, State value) {
-        updates.put(mkChannelId(group, channel), value);
-        return true;
-    }
-
-    protected void handleInput(CoIotDescrSen sen, CoIotSensor s, String rGroup, Map<String, State> updates) {
-        int idx = getSensorNumber(sen.desc, sen.id) - 1;
-        String iGroup = profile.getInputGroup(idx);
-        String iChannel = CHANNEL_INPUT + profile.getInputSuffix(idx);
-        updateChannel(updates, iGroup, iChannel, s.value == 0 ? OnOffType.OFF : OnOffType.ON);
-    }
-
-    protected void handleInputEvent(CoIotDescrSen sen, String type, int count, int serial, Map<String, State> updates) {
-        int idx = getSensorNumber(sen.desc, sen.id) - 1;
-        String group = profile.getInputGroup(idx);
-        if (count == -1) {
-            // event type
-            updateChannel(updates, group, CHANNEL_STATUS_EVENTTYPE + profile.getInputSuffix(idx), new StringType(type));
-            inputEvent[idx] = type;
-        } else {
-            // event count
-            updateChannel(updates, group, CHANNEL_STATUS_EVENTCOUNT + profile.getInputSuffix(idx), getDecimal(count));
-            logger.trace(
-                    "{}: Check button[{}] for event trigger (isButtonMode={}, 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.isButton || (profile.isButton && (serial != 0x200))) { // skip duplicate on wake-up
-                    logger.debug("{}: Trigger event {}", thingName, inputEvent[idx]);
-                    thingHandler.triggerButton(group, idx, inputEvent[idx]);
-                }
-            }
-            lastEventCount[idx] = count;
-        }
-    }
-
-    /**
-     *
-     * Handles the combined updated of the brightness channel:
-     * brightness$Switch is the OnOffType (power state)
-     * brightness&Value is the brightness value
-     *
-     * @param profile Device profile, required to select the channel group and name
-     * @param updates List of updates. updatePower will add brightness$Switch and brightness&Value if changed
-     * @param id Sensor id from the update
-     * @param sen Sensor description from the update
-     * @param s New sensor value
-     * @param allUpdatesList of updates. This is required, because we need to update both values at the same time
-     */
-    protected void updatePower(ShellyDeviceProfile profile, Map<String, State> updates, int id, CoIotDescrSen sen,
-            CoIotSensor s, List<CoIotSensor> allUpdates) {
-        String group = "";
-        String channel = CHANNEL_BRIGHTNESS;
-        String checkL = ""; // RGBW-white uses 4 different Power, Brightness, VSwitch values
-        if (profile.isLight || profile.isDimmer) {
-            if (profile.isBulb || profile.inColor) {
-                group = CHANNEL_GROUP_LIGHT_CONTROL;
-                channel = CHANNEL_LIGHT_POWER;
-            } else if (profile.isDuo) {
-                group = CHANNEL_GROUP_WHITE_CONTROL;
-            } else if (profile.isDimmer) {
-                group = CHANNEL_GROUP_RELAY_CONTROL;
-            } else if (profile.isRGBW2) {
-                checkL = String.valueOf(id); // String.valueOf(id - 1); // id is 1-based, L is 0-based
-                group = CHANNEL_GROUP_LIGHT_CHANNEL + id;
-                logger.trace("{}: updatePower() for L={}", thingName, checkL);
-            }
-
-            // We need to update brightness and on/off state at the same time to avoid "flipping brightness slider" in
-            // the UI
-            double brightness = -1.0;
-            double power = -1.0;
-            for (CoIotSensor update : allUpdates) {
-                CoIotDescrSen d = fixDescription(sensorMap.get(update.id), blkMap);
-                if (!checkL.isEmpty() && !d.links.equals(checkL)) {
-                    // continue until we find the correct one
-                    continue;
-                }
-                if (d.desc.equalsIgnoreCase("brightness")) {
-                    brightness = update.value;
-                } else if (d.desc.equalsIgnoreCase("output") || d.desc.equalsIgnoreCase("state")) {
-                    power = update.value;
-                }
-            }
-            if (power != -1) {
-                updateChannel(updates, group, channel + "$Switch", power == 1 ? OnOffType.ON : OnOffType.OFF);
-            }
-            if (brightness != -1) {
-                updateChannel(updates, group, channel + "$Value",
-                        toQuantityType(power == 1 ? brightness : 0, DIGITS_NONE, Units.PERCENT));
-            }
-        } else if (profile.hasRelays) {
-            group = profile.numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL : CHANNEL_GROUP_RELAY_CONTROL + id;
-            updateChannel(updates, group, CHANNEL_OUTPUT, s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-        } else if (profile.isSensor) {
-            // Sensor state
-            if (profile.isDW) { // Door Window has item type Contact
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
-                        s.value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
-            } else {
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
-                        s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-            }
-        }
-    }
-
-    /**
-     * Find index of Input id, which is required to map to channel name
-     *
-     * @parm sensorDesc D field from sensor update
-     * @param sensorId The id from the sensor update
-     * @return Index of found entry (+1 will be the suffix for the channel name) or null if sensorId is not found
-     */
-    protected int getSensorNumber(String sensorDesc, String sensorId) {
-        int idx = 0;
-        for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
-            CoIotDescrSen sen = se.getValue();
-            if (sen.desc.equalsIgnoreCase(sensorDesc)) {
-                idx++; // iterate from input1..2..n
-            }
-            if (sen.id.equalsIgnoreCase(sensorId) && blkMap.containsKey(sen.links)) {
-                int id = getIdFromBlk(sen);
-                if (id != -1) {
-                    return id;
-                }
-            }
-            if (sen.id.equalsIgnoreCase(sensorId)) {
-                return idx;
-            }
-        }
-        logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
-        return -1;
-    }
-
-    protected int getIdFromBlk(CoIotDescrSen sen) {
-        int idx = -1;
-        CoIotDescrBlk blk = blkMap.get(sen.links);
-        if (blk != null) {
-            String desc = blk.desc.toLowerCase();
-            if (desc.startsWith(SHELLY_CLASS_RELAY) || desc.startsWith(SHELLY_CLASS_ROLLER)
-                    || desc.startsWith(SHELLY_CLASS_LIGHT) || desc.startsWith(SHELLY_CLASS_EMETER)) {
-                if (desc.contains("_")) { // CoAP v2
-                    idx = Integer.parseInt(substringAfter(desc, "_"));
-                } else { // CoAP v1
-                    if (desc.substring(0, 5).equalsIgnoreCase(SHELLY_CLASS_RELAY)) {
-                        idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_RELAY));
-                    }
-                    if (desc.substring(0, 6).equalsIgnoreCase(SHELLY_CLASS_ROLLER)) {
-                        idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_ROLLER));
-                    }
-                    if (desc.substring(0, SHELLY_CLASS_EMETER.length()).equalsIgnoreCase(SHELLY_CLASS_EMETER)) {
-                        idx = Integer.parseInt(substringAfter(desc, SHELLY_CLASS_EMETER));
-                    }
-                }
-                idx = idx + 1; // make it 1-based (sen.L is 0-based)
-            }
-        }
-        return idx;
-    }
-
-    /**
-     *
-     * Get matching sensorId for updates on "External Temperature" - there might be more than 1 sensor.
-     *
-     * @param sensorId sensorId to map into a channel index
-     * @return Index of the corresponding channel (e.g. 0 build temperature1, 1->temperagture2...)
-     */
-    protected int getExtTempId(String sensorId) {
-        int idx = 0;
-        for (Map.Entry<String, CoIotDescrSen> se : sensorMap.entrySet()) {
-            CoIotDescrSen sen = se.getValue();
-            if (sen.desc.equalsIgnoreCase("external_temperature") || sen.desc.equalsIgnoreCase("external temperature c")
-                    || (sen.desc.equalsIgnoreCase("extTemp") && !sen.unit.equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT))) {
-                idx++; // iterate from temperature1..2..n
-            }
-            if (sen.id.equalsIgnoreCase(sensorId)) {
-                return idx;
-            }
-        }
-        logger.debug("{}: sensorId {} not found in sensorMap!", thingName, sensorId);
-        return -1;
-    }
-
-    protected ShellyDeviceProfile getProfile() {
-        return profile;
-    }
-
-    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
-        return sen != null ? sen : new CoIotDescrSen();
-    }
-
-    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
-    }
-
-    protected void addSensor(Map<String, CoIotDescrSen> sensorMap, String key, String json) {
-        try {
-            if (!sensorMap.containsKey(key)) {
-                CoIotDescrSen sen = gson.fromJson(json, CoIotDescrSen.class);
-                if (sen != null) {
-                    sensorMap.put(key, sen);
-                }
-            }
-        } catch (JsonSyntaxException e) {
-            // should never happen
-            logger.trace("Unable to parse sensor definition: {}", json, e);
-        }
-    }
-
-    public String getLastWakeup() {
-        return lastWakeup;
-    }
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion1.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion1.java
deleted file mode 100644 (file)
index 9412162..0000000
+++ /dev/null
@@ -1,375 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.unit.ImperialUnits;
-import org.openhab.core.library.unit.SIUnits;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoIoTVersion1 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
-    private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion1.class);
-
-    public ShellyCoIoTVersion1(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
-            Map<String, CoIotDescrSen> sensorMap) {
-        super(thingName, thingHandler, blkMap, sensorMap);
-    }
-
-    @Override
-    public int getVersion() {
-        return ShellyCoapJSonDTO.COIOT_VERSION_1;
-    }
-
-    /**
-     * Process CoIoT status update message. If a status update is received, but the device description has not been
-     * received yet a GET is send to query device description.
-     *
-     * @param devId device id included in the status packet
-     * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
-     * @param serial Serial for this request. If this the the same as last serial
-     *            the update was already sent and processed so this one gets
-     *            ignored.
-     */
-    @Override
-    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
-            Map<String, State> updates, ShellyColorUtils col) {
-        // first check the base implementation
-        if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
-            // process by the base class
-            return true;
-        }
-
-        // Process status information and convert into channel updates
-        Integer rIndex = Integer.parseInt(sen.links) + 1;
-        String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
-                : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
-        switch (sen.type.toLowerCase()) {
-            case "t": // Temperature +
-                Double value = getDouble(s.value);
-                switch (sen.desc.toLowerCase()) {
-                    case "temperature": // Sensor Temp
-                        if (getString(getProfile().settings.temperatureUnits)
-                                .equalsIgnoreCase(SHELLY_TEMP_FAHRENHEIT)) {
-                            value = ImperialUnits.FAHRENHEIT.getConverterTo(SIUnits.CELSIUS).convert(getDouble(s.value))
-                                    .doubleValue();
-                        }
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
-                                toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
-                        break;
-                    case "temperature f": // Device Temp -> ignore (we use C only)
-                        break;
-                    case "temperature c": // Device Temp in C
-                        // Device temperature
-                        updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
-                                toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
-                        break;
-                    case "external temperature f": // Shelly 1/1PM external temp sensors
-                        // ignore F, we use C only
-                        break;
-                    case "external temperature c": // Shelly 1/1PM external temp sensors
-                    case "external_temperature":
-                        int idx = getExtTempId(sen.id);
-                        if (idx > 0) {
-                            updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP + idx,
-                                    toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
-                        } else {
-                            logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type,
-                                    sen.desc);
-                        }
-                        break;
-                    default:
-                        logger.debug("{}: Unknown temperatur type: {}", thingName, sen.desc);
-                }
-                break;
-            case "p": // Power/Watt
-                // 3EM uses 1-based meter IDs, other 0-based
-                String mGroup = profile.numMeters == 1 ? CHANNEL_GROUP_METER
-                        : CHANNEL_GROUP_METER + (profile.isEMeter ? sen.links : rIndex);
-                updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
-                        toQuantityType(s.value, DIGITS_WATT, Units.WATT));
-                updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
-                break;
-            case "s" /* CatchAll */:
-                switch (sen.desc.toLowerCase()) {
-                    case "overtemp":
-                        if (s.value == 1) {
-                            thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
-                        }
-                        break;
-                    case "energy counter 0 [w-min]":
-                        updateChannel(updates, rGroup, CHANNEL_METER_LASTMIN1,
-                                toQuantityType(s.value, DIGITS_WATT, Units.WATT));
-                        break;
-                    case "energy counter 1 [w-min]":
-                    case "energy counter 2 [w-min]":
-                        // we don't use them
-                        break;
-                    case "energy counter total [w-h]": // 3EM reports W/h
-                    case "energy counter total [w-min]":
-                        Double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
-                        updateChannel(updates, rGroup, CHANNEL_METER_TOTALKWH,
-                                toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
-                        break;
-                    case "voltage":
-                        updateChannel(updates, rGroup, CHANNEL_EMETER_VOLTAGE,
-                                toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
-                        break;
-                    case "current":
-                        updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
-                                toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
-                        break;
-                    case "pf":
-                        updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR,
-                                toQuantityType(getDecimal(s.value), Units.PERCENT));
-                        break;
-                    case "position":
-                        // work around: Roller reports 101% instead max 100
-                        double pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min(s.value, SHELLY_MAX_ROLLER_POS));
-                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
-                                toQuantityType(SHELLY_MAX_ROLLER_POS - pos, Units.PERCENT));
-                        updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
-                                toQuantityType(pos, Units.PERCENT));
-                        break;
-                    case "input event": // Shelly Button 1
-                        handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
-                        break;
-                    case "input event counter": // Shelly Button 1/ix3
-                        handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
-                        break;
-                    case "flood":
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
-                                s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                        break;
-                    case "tilt": // DW with FW1.6.5+ //+
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
-                                toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
-                        break;
-                    case "vibration": // DW with FW1.6.5+
-                        if (profile.isMotion) {
-                            // handle as status
-                            updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
-                                    s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                        } else if (s.value == 1) {
-                            // handle as event
-                            thingHandler.triggerChannel(CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
-                                    EVENT_TYPE_VIBRATION);
-                        }
-                        break;
-                    case "temp": // Shelly Bulb
-                    case "colortemperature": // Shelly Duo
-                        updateChannel(updates,
-                                profile.inColor ? CHANNEL_GROUP_COLOR_CONTROL : CHANNEL_GROUP_WHITE_CONTROL,
-                                CHANNEL_COLOR_TEMP,
-                                ShellyColorUtils.toPercent((int) s.value, profile.minTemp, profile.maxTemp));
-                        break;
-                    case "sensor state": // Shelly Gas
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
-                        break;
-                    case "alarm state": // Shelly Gas
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE,
-                                getStringType(s.valueStr));
-                        break;
-                    case "self-test state":// Shelly Gas
-                        updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST,
-                                getStringType(s.valueStr));
-                        break;
-                    case "concentration":// Shelly Gas
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM,
-                                toQuantityType(getDouble(s.value), DIGITS_NONE, Units.PARTS_PER_MILLION));
-                        break;
-                    case "sensorerror":
-                        updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ERROR, getStringType(s.valueStr));
-                        break;
-                    default:
-                        // Unknown
-                        return false;
-                }
-                break;
-
-            default:
-                // Unknown type
-                return false;
-        }
-        return true;
-    }
-
-    /**
-     *
-     * Depending on the device type and firmware release there are significant bugs or incosistencies in the CoIoT
-     * Device Description returned by the discovery request. Shelly is even not following it's own speicifcation. All of
-     * that has been reported to Shelly and acknowledged. Firmware 1.6 brought significant improvements. However, the
-     * old mapping stays in to support older firmware releases.
-     *
-     * @param sen Sensor description received from device
-     * @return fixed Sensor description (sen)
-     */
-    @Override
-    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
-        // Shelly1: reports null descr+type "Switch" -> map to S
-        // Shelly1PM: reports null descr+type "Overtemp" -> map to O
-        // Shelly1PM: reports null descr+type "W" -> add description
-        // Shelly1PM: reports temp senmsors without desc -> add description
-        // Shelly Dimmer: sensors are reported without descriptions -> map to S
-        // SHelly Sense: multiple issues: Description should not be lower case, invalid type for Motion and Battery
-        // Shelly Sense: Battery is reported with Desc "battery", but type "H" instead of "B"
-        // Shelly Sense: Motion is reported with Desc "battery", but type "H" instead of "B"
-        // Shelly Bulb: Colors are coded with Type="Red" etc. rather than Type="S" and color as Descr
-        // Shelly RGBW2 is reporting Brightness, Power, VSwitch for each channel, but all with L=0
-        if (sen == null) {
-            throw new IllegalArgumentException("sen should not be null!");
-        }
-        if (sen.desc == null) {
-            sen.desc = "";
-        }
-        String desc = sen.desc.toLowerCase();
-
-        // RGBW2 reports Power_0, Power_1, Power_2, Power_3; same for VSwitch and Brightness, all of them linkted to L:0
-        // we break it up to Power with L:0, Power with L:1...
-        if (desc.contains("_") && (desc.contains("power") || desc.contains("vswitch") || desc.contains("brightness"))) {
-            String newDesc = substringBefore(sen.desc, "_");
-            String newLink = substringAfter(sen.desc, "_");
-            sen.desc = newDesc;
-            sen.links = newLink;
-            if (!blkMap.containsKey(sen.links)) {
-                // auto-insert a matching blk entry
-                CoIotDescrBlk blk = new CoIotDescrBlk();
-                CoIotDescrBlk blk0 = blkMap.get("0"); // blk 0 is always there
-                blk.id = sen.links;
-                if (blk0 != null) {
-                    blk.desc = blk0.desc + "_" + blk.id;
-                    blkMap.put(blk.id, blk);
-                }
-            }
-        }
-
-        switch (sen.type.toLowerCase()) {
-            case "w": // old devices/firmware releases use "W", new ones "P"
-                sen.type = "P";
-                sen.desc = "Power";
-                break;
-            case "tc":
-                sen.type = "T";
-                sen.desc = "Temperature C";
-                break;
-            case "tf":
-                sen.type = "T";
-                sen.desc = "Temperature F";
-                break;
-            case "overtemp":
-                sen.type = "S";
-                sen.desc = "Overtemp";
-                break;
-            case "relay0":
-            case "switch":
-            case "vswitch":
-                sen.type = "S";
-                sen.desc = "State";
-                break;
-        }
-
-        switch (sen.desc.toLowerCase()) {
-            case "motion": // fix acc to spec it's T=M
-                sen.type = "M";
-                sen.desc = "Motion";
-                break;
-            case "battery": // fix: type is B not H
-                sen.type = "B";
-                sen.desc = "Battery";
-                break;
-            case "overtemp":
-                sen.type = "S";
-                sen.desc = "Overtemp";
-                break;
-            case "relay0":
-            case "switch":
-            case "vswitch":
-                sen.type = "S";
-                sen.desc = "State";
-                break;
-            case "e cnt 0 [w-min]": // 4 Pro
-            case "e cnt 1 [w-min]":
-            case "e cnt 2 [w-min]":
-            case "e cnt total [w-min]": // 4 Pro
-                sen.desc = sen.desc.toLowerCase().replace("e cnt", "energy counter");
-                break;
-
-        }
-
-        if (sen.desc.isEmpty()) {
-            switch (sen.type.toLowerCase()) {
-                case "p":
-                    sen.desc = "Power";
-                    break;
-                case "T":
-                    sen.desc = "Temperature";
-                    break;
-                case "input":
-                    sen.type = "S";
-                    sen.desc = "Input";
-                    break;
-                case "output":
-                    sen.type = "S";
-                    sen.desc = "Output";
-                    break;
-                case "brightness":
-                    sen.type = "S";
-                    sen.desc = "Brightness";
-                    break;
-                case "red":
-                case "green":
-                case "blue":
-                case "white":
-                case "gain":
-                case "temp": // Bulb: Color temperature
-                    sen.desc = sen.type;
-                    sen.type = "S";
-                    break;
-                case "vswitch":
-                    // it seems that Shelly tends to break their own spec: T is the description and D is no longer
-                    // included -> map D to sen.T and set CatchAll for T
-                    sen.desc = sen.type;
-                    sen.type = "S";
-                    break;
-                // Default: set no description
-                // (there are no T values defined in the CoIoT spec)
-                case "tostate":
-                default:
-                    sen.desc = "";
-            }
-        }
-        return sen;
-    }
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoIoTVersion2.java
deleted file mode 100644 (file)
index efbe077..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-/**
- * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 1
- *
- * @author Markus Michels - Initial contribution
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.OpenClosedType;
-import org.openhab.core.library.unit.SIUnits;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link ShellyCoIoTVersion1} implements the parsing for CoIoT version 2
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoIoTVersion2 extends ShellyCoIoTProtocol implements ShellyCoIoTInterface {
-    private final Logger logger = LoggerFactory.getLogger(ShellyCoIoTVersion2.class);
-
-    public ShellyCoIoTVersion2(String thingName, ShellyThingInterface thingHandler, Map<String, CoIotDescrBlk> blkMap,
-            Map<String, CoIotDescrSen> sensorMap) {
-        super(thingName, thingHandler, blkMap, sensorMap);
-    }
-
-    @Override
-    public int getVersion() {
-        return ShellyCoapJSonDTO.COIOT_VERSION_2;
-    }
-
-    /**
-     * Process CoIoT status update message. If a status update is received, but the device description has not been
-     * received yet a GET is send to query device description.
-     *
-     * @param sensorUpdates Complete list of sensor updates
-     * @param sen The specific sensor update to handle
-     * @param updates Resulting updates (new updates will be added to input list)
-     */
-    @Override
-    public boolean handleStatusUpdate(List<CoIotSensor> sensorUpdates, CoIotDescrSen sen, int serial, CoIotSensor s,
-            Map<String, State> updates, ShellyColorUtils col) {
-        // first check the base implementation
-        if (super.handleStatusUpdate(sensorUpdates, sen, s, updates, col)) {
-            // process by the base class
-            return true;
-        }
-
-        // Process status information and convert into channel updates
-        // Integer rIndex = Integer.parseInt(sen.links) + 1;
-        int rIndex = getIdFromBlk(sen);
-        String rGroup = getProfile().numRelays <= 1 ? CHANNEL_GROUP_RELAY_CONTROL
-                : CHANNEL_GROUP_RELAY_CONTROL + rIndex;
-        String mGroup = profile.numMeters <= 1 ? CHANNEL_GROUP_METER
-                : CHANNEL_GROUP_METER + (profile.isEMeter ? getIdFromBlk(sen) : rIndex);
-
-        boolean processed = true;
-        double value = getDouble(s.value);
-        String reason = "";
-
-        if (profile.isTRV) {
-            // Special handling for TRV, because it uses duplicate ID values with different meanings
-            switch (sen.id) {
-                case "3101": // current temp
-                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TEMP,
-                            toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
-                    break;
-                case "3103": // target temp in C. 4/31, 999=unknown
-                    updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_SETTEMP,
-                            toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
-                    break;
-                case "3116": // S, valveError, 0/1
-                    if (s.value == 1) {
-                        thingHandler.postEvent(ALARM_TYPE_VALVE_ERROR, false);
-                    }
-                    break;
-                case "3117": // S, mode, 0-5 (0=disabled)
-                    value = getDouble(s.value).intValue();
-                    updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_PROFILE, getDecimal(value));
-                    updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SCHEDULE, getOnOff(value > 0));
-                    break;
-                case "3118": // Valve state
-                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
-                            value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
-                    break;
-                case "3121": // valvePos, Type=S, Range=0/100;
-                    boolean updated = updateChannel(updates, CHANNEL_GROUP_CONTROL, CHANNEL_CONTROL_POSITION,
-                            s.value != -1 ? toQuantityType(getDouble(s.value), 0, Units.PERCENT) : UnDefType.UNDEF);
-                    if (updated && s.value >= 0 && s.value != thingHandler.getChannelDouble(CHANNEL_GROUP_CONTROL,
-                            CHANNEL_CONTROL_POSITION)) {
-                        logger.debug("{}: Valve position changed, force update", thingName);
-                        thingHandler.requestUpdates(1, false);
-                    }
-                    break;
-                default:
-                    processed = false;
-            }
-        } else {
-            processed = false;
-        }
-
-        if (processed) {
-            return true;
-        }
-
-        processed = true;
-        switch (sen.id) {
-            case "3106": // L, luminosity, lux, U32, -1
-            case "3110": // S, luminosityLevel, dark/twilight/bright, "unknown"=unknown
-            case "3111": // B, battery, 0-100%, unknown -1
-            case "3112": // S, charger, 0/1
-            case "3115": // S, sensorError, 0/1
-                // processed by base handler
-                break;
-
-            case "6109": // P, overpowerValue, W, U32
-            case "9101":
-                // Relay: S, mode, relay/roller or
-                // Dimmer: S, mode, color/white
-                // skip, could check against thing mode...
-                break;
-
-            case "1101": // relay_0: output, 0/1
-            case "1201": // relay_1: output, 0/1
-            case "1301": // relay_2: output, 0/1
-            case "1401": // relay_3: output, 0/1
-                updatePower(profile, updates, rIndex, sen, s, sensorUpdates);
-                break;
-            case "1102": // roler_0: S, roller, open/close/stop -> roller state
-                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STATE, getStringType(s.valueStr));
-                break;
-            case "1103": // roller_0: S, rollerPos, 0-100, unknown -1
-                int pos = Math.max(SHELLY_MIN_ROLLER_POS, Math.min((int) value, SHELLY_MAX_ROLLER_POS));
-                logger.debug("{}: CoAP update roller position: control={}, position={}", thingName,
-                        SHELLY_MAX_ROLLER_POS - pos, pos);
-                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_CONTROL,
-                        toQuantityType((double) (SHELLY_MAX_ROLLER_POS - pos), Units.PERCENT));
-                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_POS,
-                        toQuantityType((double) pos, Units.PERCENT));
-                break;
-            case "1105": // Gas: S, valve, closed/opened/not_connected/failure/closing/opening/checking or unknown
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VALVE, getStringType(s.valueStr));
-                break;
-
-            case "2101": // Input_0: S, input, 0/1
-            case "2201": // Input_1: S, input, 0/1
-            case "2301": // Input_2: S, input, 0/1
-            case "2401": // Input_3: S, input, 0/1
-                handleInput(sen, s, rGroup, updates);
-                break;
-            case "2102": // Input_0: EV, inputEvent, S/SS/SSS/L
-            case "2202": // Input_1: EV, inputEvent
-            case "2302": // Input_2: EV, inputEvent
-            case "2402": // Input_3: EV, inputEvent
-                handleInputEvent(sen, getString(s.valueStr), -1, serial, updates);
-                break;
-            case "2103": // EVC, inputEventCnt, U16
-            case "2203": // EVC, inputEventCnt, U16
-            case "2303": // EVC, inputEventCnt, U16
-            case "2403": // EVC, inputEventCnt, U16
-                handleInputEvent(sen, "", getInteger((int) s.value), serial, updates);
-                break;
-            case "3101": // sensor_0: T, extTemp, C, -55/125; unknown 999
-            case "3201": // sensor_1: T, extTemp, C, -55/125; unknown 999
-            case "3301": // sensor_2: T, extTemp, C, -55/125; unknown 999
-                int idx = getExtTempId(sen.id);
-                if (idx >= 0) {
-                    // H&T, Fllod, DW only have 1 channel, 1/1PM with Addon have up to to 3 sensors
-                    String channel = profile.isSensor ? CHANNEL_SENSOR_TEMP : CHANNEL_SENSOR_TEMP + idx;
-                    // Some devices report values = -999 or 99 during fw update
-                    updateChannel(updates, CHANNEL_GROUP_SENSOR, channel,
-                            toQuantityType(value, DIGITS_TEMP, SIUnits.CELSIUS));
-                } else {
-                    logger.debug("{}: Unable to get extSensorId {} from {}/{}", thingName, sen.id, sen.type, sen.desc);
-                }
-                break;
-            case "3104": // T, deviceTemp, Celsius -40/300; 999=unknown
-                if ("targetTemp".equalsIgnoreCase(sen.desc)) {
-
-                    break; // target temp in F-> ignore
-                }
-                // sensor_0: T, internalTemp, F, 39/88, unknown 999
-                updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ITEMP,
-                        toQuantityType(value, DIGITS_NONE, SIUnits.CELSIUS));
-                break;
-            case "3102": // sensor_0: T, extTemp, F, -67/257, unknown 999
-            case "3202": // sensor_1: T, extTemp, F, -67/257, unknown 999
-            case "3302": // sensor_2: T, extTemp, F, -67/257, unknown 999
-            case "3105": // T, deviceTemp, Fahrenheit -40/572
-                // skip, we use only C
-                break;
-
-            case "3107": // C, Gas concentration, U16
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_PPM, getDecimal(s.value));
-                break;
-            case "3108": // DW: S, dwIsOpened, 0/1, -1=unknown
-                if (value != -1) {
-                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_STATE,
-                            value != 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
-                } else {
-                    logger.debug("{}: Sensor error reported, check device, battery and installation", thingName);
-                }
-                break;
-            case "3109": // S, tilt, 0-180deg, -1
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_TILT,
-                        toQuantityType(s.value, DIGITS_NONE, Units.DEGREE_ANGLE));
-                break;
-            case "3113": // S, sensorOp, warmup/normal/fault
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_SSTATE, getStringType(s.valueStr));
-                break;
-            case "3114": // S, selfTest, not_completed/completed/running/pending
-                updateChannel(updates, CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_SELFTTEST, getStringType(s.valueStr));
-                break;
-            case "3117": // S, extInput, 0/1
-                handleInput(sen, s, rGroup, updates);
-                break;
-            case "3118":
-                updateChannel(updates, mGroup, CHANNEL_SENSOR_VOLTAGE,
-                        toQuantityType(getDouble(s.value), 2, Units.VOLT));
-                break;
-
-            case "4101": // relay_0/light_0: P, power, W
-            case "4201": // relay_1/light_1: P, power, W
-            case "4301": // relay_2/light_2: P, power, W
-            case "4401": // relay_3/light_3: P, power, W
-            case "4105": // emeter_0: P, power, W
-            case "4205": // emeter_1: P, power, W
-            case "4305": // emeter_2: P, power, W
-            case "4102": // roller_0: P, rollerPower, W, 0-2300, unknown -1
-            case "4202": // roller_1: P, rollerPower, W, 0-2300, unknown -1
-                updateChannel(updates, mGroup, CHANNEL_METER_CURRENTWATTS,
-                        toQuantityType(s.value, DIGITS_WATT, Units.WATT));
-                if (!profile.isRGBW2 && !profile.isRoller) {
-                    // only for regular, not-aggregated meters
-                    updateChannel(updates, mGroup, CHANNEL_LAST_UPDATE, getTimestamp());
-                }
-                break;
-
-            case "4103": // relay_0: E, energy, Wmin, U32
-            case "4203": // relay_1: E, energy, Wmin, U32
-            case "4303": // relay_2: E, energy, Wmin, U32
-            case "4403": // relay_3: E, energy, Wmin, U32
-            case "4104": // roller_0: E, rollerEnergy, Wmin, U32, -1
-            case "4204": // roller_0: E, rollerEnergy, Wmin, U32, -1
-            case "4106": // emeter_0: E, energy, Wh, U32
-            case "4206": // emeter_1: E, energy, Wh, U32
-            case "4306": // emeter_2: E, energy, Wh, U32
-                double total = profile.isEMeter ? s.value / 1000 : s.value / 60 / 1000;
-                updateChannel(updates, mGroup, CHANNEL_METER_TOTALKWH,
-                        toQuantityType(total, DIGITS_KWH, Units.KILOWATT_HOUR));
-                break;
-
-            case "4107": // emeter_0: E, energyReturned, Wh, U32, -1
-            case "4207": // emeter_1: E, energyReturned, Wh, U32, -1
-            case "4307": // emeter_2: E, energyReturned, Wh, U32, -1
-                updateChannel(updates, mGroup, CHANNEL_EMETER_TOTALRET,
-                        toQuantityType(getDouble(s.value) / 1000, DIGITS_KWH, Units.KILOWATT_HOUR));
-                break;
-
-            case "4108": // emeter_0: V, voltage, 0-265V, U32, -1
-            case "4208": // emeter_1: V, voltage, 0-265V, U32, -1
-            case "4308": // emeter_2: V, voltage, 0-265V, U32, -1
-                updateChannel(updates, mGroup, CHANNEL_EMETER_VOLTAGE,
-                        toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.VOLT));
-                break;
-
-            case "4109": // emeter_0: A, current, 0/120A, -1
-            case "4209": // emeter_1: A, current, 0/120A, -1
-            case "4309": // emeter_2: A, current, 0/120A, -1
-                updateChannel(updates, rGroup, CHANNEL_EMETER_CURRENT,
-                        toQuantityType(getDouble(s.value), DIGITS_VOLT, Units.AMPERE));
-                break;
-
-            case "4110": // emeter_0: S, powerFactor, 0/1, -1
-            case "4210": // emeter_1: S, powerFactor, 0/1, -1
-            case "4310": // emeter_2: S, powerFactor, 0/1, -1
-                updateChannel(updates, rGroup, CHANNEL_EMETER_PFACTOR, getDecimal(s.value));
-                break;
-
-            case "5101": // {"I":5101,"T":"S","D":"brightness","R":"0/100","L":1},
-            case "5102": // {"I":5102,"T":"S","D":"gain","R":"0/100","L":1},
-            case "5103": // {"I":5103,"T":"S","D":"colorTemp","U":"K","R":"3000/6500","L":1},
-            case "5105": // {"I":5105,"T":"S","D":"red","R":"0/255","L":1},
-            case "5106": // {"I":5106,"T":"S","D":"green","R":"0/255","L":1},
-            case "5107": // {"I":5107,"T":"S","D":"blue","R":"0/255","L":1},
-            case "5108": // {"I":5108,"T":"S","D":"white","R":"0/255","L":1},
-                // already covered by base handler
-                break;
-
-            case "6101": // A, overtemp, 0/1
-                if (s.value == 1) {
-                    thingHandler.postEvent(ALARM_TYPE_OVERTEMP, true);
-                }
-                break;
-            case "6102": // relay_0: A, overpower, 0/1
-            case "6202": // relay_1: A, overpower, 0/1
-            case "6302": // relay_2: A, overpower, 0/1
-            case "6402": // relay_3: A, overpower, 0/1
-                if (s.value == 1) {
-                    thingHandler.postEvent(ALARM_TYPE_OVERPOWER, true);
-                }
-                break;
-            case "6104": // relay_0: A, loadError, 0/1
-            case "6204": // relay_1: A, loadError, 0/1
-            case "6304": // relay_2: A, loadError, 0/1
-            case "6404": // relay_3: A, loadError, 0/1
-                if (s.value == 1) {
-                    thingHandler.postEvent(ALARM_TYPE_LOADERR, true);
-                }
-                break;
-            case "6103": // roller_0: A, rollerStopReason, normal/safety_switch/obstacle/overpower
-                reason = getString(s.valueStr);
-                updateChannel(updates, CHANNEL_GROUP_ROL_CONTROL, CHANNEL_ROL_CONTROL_STOPR, getStringType(reason));
-                if (!reason.isEmpty() && !reason.equalsIgnoreCase(SHELLY_API_STOPR_NORMAL)) {
-                    thingHandler.postEvent("ROLLER_" + reason.toUpperCase(), true);
-                }
-            case "6106": // A, flood, 0/1, -1
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_FLOOD,
-                        value == 1 ? OnOffType.ON : OnOffType.OFF);
-                break;
-
-            case "6107": // A, motion, 0/1, -1
-                // {"I":6107,"T":"A","D":"motion","R":["0/1","-1"],"L":1},
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION,
-                        value == 1 ? OnOffType.ON : OnOffType.OFF);
-                break;
-            case "3119": // Motion timestamp (timestamp os GMT, not adapted to the adapted timezone)
-                // {"I":3119,"T":"S","D":"timestamp","U":"s","R":["U32","-1"],"L":1},
-                if (s.value != 0) {
-                    updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_TS,
-                            getTimestamp(getString("GMT"), (long) s.value));
-                }
-                break;
-            case "3120": // motionActive (timestamp os GMT, not adapted to the adapted timezone)
-                // {"I":3120,"T":"S","D":"motionActive","R":["0/1","-1"],"L":1},
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_MOTION_ACT,
-                        getTimestamp("GMT", (long) s.value));
-                break;
-
-            case "6108": // A, gas, none/mild/heavy/test or unknown
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_ALARM_STATE, getStringType(s.valueStr));
-                break;
-            case "6110": // A, vibration, 0/1, -1=unknown
-                updateChannel(updates, CHANNEL_GROUP_SENSOR, CHANNEL_SENSOR_VIBRATION,
-                        s.value == 1 ? OnOffType.ON : OnOffType.OFF);
-                if (s.value == 1) {
-                    // post event
-                    thingHandler.triggerChannel(CHANNEL_GROUP_DEV_STATUS, CHANNEL_DEVST_ALARM, EVENT_TYPE_VIBRATION);
-                }
-                break;
-            case "9102": // EV, wakeupEvent, battery/button/periodic/poweron/sensor/ext_power, "unknown"=unknown
-                if (s.valueArray.size() > 0) {
-                    thingHandler.updateWakeupReason(s.valueArray);
-                    lastWakeup = (String) s.valueArray.get(0);
-                }
-                break;
-            case "9103": // EVC, cfgChanged, U16
-                if (lastCfgCount == -1 || lastCfgCount != s.value) {
-                    thingHandler.requestUpdates(1, true); // refresh config
-                }
-                lastCfgCount = (int) s.value;
-                break;
-
-            default:
-                processed = false;
-        }
-        return processed;
-    }
-
-    @Override
-    public CoIotDescrSen fixDescription(@Nullable CoIotDescrSen sen, Map<String, CoIotDescrBlk> blkMap) {
-        return super.fixDescription(sen, blkMap);
-    }
-
-    private static final String ID_4101_DESCR = "{ \"I\":4101, \"T\":\"P\", \"D\":\"power\",  \"U\": \"W\",    \"R\":\"0/3500\", \"L\": 1}";
-    private static final String ID_4103_DESCR = "{ \"I\":4103, \"T\":\"E\", \"D\":\"energy\", \"U\": \"Wmin\", \"R\":\"U32\", \"L\": 1}";
-
-    @Override
-    public void completeMissingSensorDefinition(Map<String, CoIotDescrSen> sensorMap) {
-        if (profile.isDuo && profile.inColor) {
-            addSensor(sensorMap, "4101", ID_4101_DESCR);
-            addSensor(sensorMap, "4103", ID_4103_DESCR);
-        }
-        super.completeMissingSensorDefinition(sensorMap);
-    }
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapHandler.java
deleted file mode 100644 (file)
index 9b466c9..0000000
+++ /dev/null
@@ -1,681 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.*;
-import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
-
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.eclipse.californium.core.CoapClient;
-import org.eclipse.californium.core.coap.CoAP.Code;
-import org.eclipse.californium.core.coap.CoAP.ResponseCode;
-import org.eclipse.californium.core.coap.CoAP.Type;
-import org.eclipse.californium.core.coap.MessageObserverAdapter;
-import org.eclipse.californium.core.coap.Option;
-import org.eclipse.californium.core.coap.OptionNumberRegistry;
-import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.californium.core.network.Endpoint;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescription;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotGenericSensorList;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensorTypeAdapter;
-import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
-import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
-import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * The {@link ShellyCoapHandler} handles the CoIoT/CoAP registration and events.
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoapHandler implements ShellyCoapListener {
-    private static final byte[] EMPTY_BYTE = new byte[0];
-
-    private final Logger logger = LoggerFactory.getLogger(ShellyCoapHandler.class);
-    private final ShellyThingInterface thingHandler;
-    private ShellyThingConfiguration config = new ShellyThingConfiguration();
-    private final GsonBuilder gsonBuilder = new GsonBuilder();
-    private final Gson gson;
-    private String thingName;
-
-    private boolean coiotBound = false;
-    private ShellyCoIoTInterface coiot;
-    private int coiotVers = -1;
-
-    private final ShellyCoapServer coapServer;
-    private @Nullable CoapClient statusClient;
-    private Request reqDescription = new Request(Code.GET, Type.CON);
-    private Request reqStatus = new Request(Code.GET, Type.CON);
-    private boolean updatesRequested = false;
-    private int coiotPort = COIOT_PORT;
-
-    private long coiotMessages = 0;
-    private long coiotErrors = 0;
-    private int lastSerial = -1;
-    private String lastPayload = "";
-    private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
-    private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
-    private ShellyDeviceProfile profile;
-    private ShellyApiInterface api;
-
-    public ShellyCoapHandler(ShellyThingInterface thingHandler, ShellyCoapServer coapServer) {
-        this.thingHandler = thingHandler;
-        this.thingName = thingHandler.getThingName();
-        this.profile = thingHandler.getProfile();
-        this.api = thingHandler.getApi();
-        this.coapServer = coapServer;
-        this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
-
-        gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
-        gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
-        gson = gsonBuilder.create();
-    }
-
-    /**
-     * Initialize CoAP access, send discovery packet and start Status server
-     *
-     * @parm thingName Thing name derived from Thing Type/hostname
-     * @parm config ShellyThingConfiguration
-     * @thows ShellyApiException
-     */
-    public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
-        try {
-            this.thingName = thingName;
-            this.config = config;
-            this.profile = thingHandler.getProfile();
-            if (isStarted()) {
-                logger.trace("{}: CoAP Listener was already started", thingName);
-                stop();
-            }
-
-            logger.debug("{}: Starting CoAP Listener", thingName);
-            if (!profile.coiotEndpoint.isEmpty() && profile.coiotEndpoint.contains(":")) {
-                String ps = substringAfter(profile.coiotEndpoint, ":");
-                coiotPort = Integer.parseInt(ps);
-            }
-            coapServer.start(config.localIp, coiotPort, this);
-            statusClient = new CoapClient(completeUrl(config.deviceIp, coiotPort, COLOIT_URI_DEVSTATUS))
-                    .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
-            @Nullable
-            Endpoint endpoint = null;
-            CoapClient client = statusClient;
-            if (client != null) {
-                endpoint = client.getEndpoint();
-            }
-            if ((endpoint == null) || !endpoint.isStarted()) {
-                logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
-                throw new ShellyApiException("Network initialization failed");
-            }
-
-            discover();
-        } catch (SocketException e) {
-            logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
-            throw new ShellyApiException("Network error", e);
-        } catch (UnknownHostException e) {
-            logger.info("{}: CoAP Exception (Unknown Host)", thingName, e);
-            throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
-        }
-    }
-
-    public boolean isStarted() {
-        return statusClient != null;
-    }
-
-    /**
-     * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
-     *
-     * @param response The Response packet
-     */
-    @Override
-    public void processResponse(@Nullable Response response) {
-        if (response == null) {
-            coiotErrors++;
-            return; // other device instance
-        }
-        ResponseCode code = response.getCode();
-        if (code != ResponseCode.CONTENT) {
-            // error handling
-            logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
-                    response.getPayloadString());
-            coiotErrors++;
-            return;
-        }
-
-        List<Option> options = response.getOptions().asSortedList();
-        String ip = response.getSourceContext().getPeerAddress().toString();
-        boolean match = ip.contains(config.deviceIp);
-        if (!match) {
-            // We can't identify device by IP, so we need to check the CoAP header's Global Device ID
-            for (Option opt : options) {
-                if (opt.getNumber() == COIOT_OPTION_GLOBAL_DEVID) {
-                    String devid = opt.getStringValue();
-                    if (devid.contains("#")) {
-                        // Format: <device type>#<mac address>#<coap version>
-                        String macid = substringBetween(devid, "#", "#");
-                        if (profile.mac.toUpperCase().contains(macid.toUpperCase())) {
-                            match = true;
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-        if (!match) {
-            // other instance
-            return;
-        }
-
-        String payload = "";
-        String devId = "";
-        String uri = "";
-        int serial = -1;
-        try {
-            coiotMessages++;
-            if (logger.isDebugEnabled()) {
-                logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
-                        response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
-            }
-            if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
-                logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
-                coiotErrors++;
-                return;
-            }
-
-            payload = response.getPayloadString();
-            for (Option opt : options) {
-                switch (opt.getNumber()) {
-                    case OptionNumberRegistry.URI_PATH:
-                        uri = COLOIT_URI_BASE + opt.getStringValue();
-                        break;
-                    case OptionNumberRegistry.URI_HOST: // ignore
-                        break;
-                    case OptionNumberRegistry.CONTENT_FORMAT: // ignore
-                        break;
-                    case COIOT_OPTION_GLOBAL_DEVID:
-                        devId = opt.getStringValue();
-                        String sVersion = substringAfterLast(devId, "#");
-                        int iVersion = Integer.parseInt(sVersion);
-                        if (coiotBound && (coiotVers != iVersion)) {
-                            logger.debug("{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
-                                    thingName, coiotVers, iVersion);
-                            thingHandler.reinitializeThing();
-                            coiotBound = false;
-                        }
-                        if (!coiotBound) {
-                            thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
-                            logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
-                            if (iVersion == COIOT_VERSION_1) {
-                                coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
-                            } else if (iVersion == COIOT_VERSION_2) {
-                                coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
-                            } else {
-                                logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
-                                return;
-                            }
-                            coiotVers = iVersion;
-                            coiotBound = true;
-                        }
-                        break;
-                    case COIOT_OPTION_STATUS_VALIDITY:
-                        break;
-                    case COIOT_OPTION_STATUS_SERIAL:
-                        serial = opt.getIntegerValue();
-                        break;
-                    default:
-                        logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
-                                opt.getValue());
-                }
-            }
-
-            // If we received a CoAP message successful the thing must be online
-            thingHandler.setThingOnline();
-
-            // The device changes the serial on every update, receiving a message with the same serial is a
-            // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
-            if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
-                    || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
-                logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
-                return;
-            }
-
-            // fixed malformed JSON :-(
-            payload = fixJSON(payload);
-
-            try {
-                if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC) || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
-                    handleDeviceDescription(devId, payload);
-                } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
-                        || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
-                    handleStatusUpdate(devId, payload, serial);
-                }
-            } catch (ShellyApiException e) {
-                logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
-                coiotErrors++;
-            }
-
-            if (!updatesRequested) {
-                // Observe Status Updates
-                reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
-                updatesRequested = true;
-            }
-        } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
-            logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
-            resetSerial();
-            coiotErrors++;
-        }
-    }
-
-    /**
-     * Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
-     * etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
-     * status updates to the device units and matching the correct thing channel.
-     *
-     * @param devId The device id reported in the CoIoT message.
-     * @param payload Device desciption in JSon format, example:
-     *            {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
-     */
-    private void handleDeviceDescription(String devId, String payload) throws ShellyApiException {
-        logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
-
-        try {
-            boolean valid = true;
-
-            // Decode Json
-            CoIotDevDescription descr = fromJson(gson, payload, CoIotDevDescription.class);
-            for (int i = 0; i < descr.blk.size(); i++) {
-                CoIotDescrBlk blk = descr.blk.get(i);
-                logger.debug("{}:    id={}: {}", thingName, blk.id, blk.desc);
-                if (!blkMap.containsKey(blk.id)) {
-                    blkMap.put(blk.id, blk);
-                } else {
-                    blkMap.replace(blk.id, blk);
-                }
-                if ((blk.type != null) && !blk.type.isEmpty()) {
-                    // in fact it is a sen entry - that's vioaling the Spec
-                    logger.trace("{}:    fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
-                            blk.desc);
-                    CoIotDescrSen sen = new CoIotDescrSen();
-                    sen.id = blk.id;
-                    sen.desc = blk.desc;
-                    sen.type = blk.type;
-                    sen.range = blk.range;
-                    sen.links = blk.links;
-                    valid &= addSensor(sen);
-                }
-            }
-
-            // Save to thing properties
-            thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
-
-            logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
-            if (descr.sen != null) {
-                for (int i = 0; i < descr.sen.size(); i++) {
-                    valid &= addSensor(descr.sen.get(i));
-                }
-            }
-            coiot.completeMissingSensorDefinition(sensorMap);
-
-            if (!valid) {
-                logger.debug(
-                        "{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
-                        thingName, coiot.getVersion());
-
-                discover();
-                return;
-            }
-        } catch (JsonSyntaxException e) {
-            logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
-        } catch (NullPointerException | IllegalArgumentException e) {
-            logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
-        }
-    }
-
-    /**
-     * Add a new sensor to the sensor table
-     *
-     * @param sen CoIotDescrSen of the sensor
-     */
-    private synchronized boolean addSensor(CoIotDescrSen sen) {
-        logger.debug("{}:    id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
-                sen.links);
-        // CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
-        // We need to make sure that the persisted device description matches,
-        // otherwise the stored one is discarded and a new discovery is triggered
-        // This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
-        int vers = coiot.getVersion();
-        if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
-                || ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
-            logger.debug("{}: Invalid format for sensor defition detected, id={}", thingName, sen.id);
-            return false;
-        }
-
-        try {
-            CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
-            if (!sensorMap.containsKey(fixed.id)) {
-                sensorMap.put(sen.id, fixed);
-            } else {
-                sensorMap.replace(sen.id, fixed);
-            }
-        } catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
-                                                                      // description is buggy
-            logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
-        }
-
-        return true;
-    }
-
-    /**
-     * Process CoIoT status update message. If a status update is received, but the device description has not been
-     * received yet a GET is send to query device description.
-     *
-     * @param devId device id included in the status packet
-     * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
-     * @param serial Serial for this request. If this the the same as last serial
-     *            the update was already sent and processed so this one gets
-     *            ignored.
-     * @throws ShellyApiException
-     */
-    private void handleStatusUpdate(String devId, String payload, int serial) throws ShellyApiException {
-        logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
-        if (blkMap.isEmpty()) {
-            // send discovery packet
-            resetSerial();
-            discover();
-
-            // try to uses description from last initialization
-            String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
-            if (savedDescr.isEmpty()) {
-                logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
-                return;
-            }
-
-            // simulate received device description to create element table
-            logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
-            handleDeviceDescription(devId, savedDescr);
-        }
-
-        // Parse Json,
-        CoIotGenericSensorList list = fromJson(gson, fixJSON(payload), CoIotGenericSensorList.class);
-        if (list.generic == null) {
-            logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
-            return;
-        }
-
-        List<CoIotSensor> sensorUpdates = list.generic;
-        Map<String, State> updates = new TreeMap<String, State>();
-        logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
-        int failed = 0;
-        ShellyColorUtils col = new ShellyColorUtils();
-        for (int i = 0; i < sensorUpdates.size(); i++) {
-            try {
-                CoIotSensor s = sensorUpdates.get(i);
-                CoIotDescrSen sen = sensorMap.get(s.id);
-                if (sen == null) {
-                    logger.debug("{}: Unable to sensor definition for id={}, payload={}", thingName, s.id, payload);
-                    continue;
-                }
-                // find matching sensor definition from device description, use the Link ID as index
-                CoIotDescrBlk element = null;
-                sen = coiot.fixDescription(sen, blkMap);
-                element = blkMap.get(sen.links);
-                if (element == null) {
-                    logger.debug("{}: Unable to find BLK for link {} from sen.id={}, payload={}", thingName, sen.links,
-                            sen.id, payload);
-                    continue;
-                }
-                logger.trace("{}:  Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
-                        i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
-                        sen.links, element.desc);
-
-                if (!coiot.handleStatusUpdate(sensorUpdates, sen, serial, s, updates, col)) {
-                    logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
-                            sen.id, sen.type, sen.desc, s.value, payload);
-                }
-            } catch (NullPointerException | IllegalArgumentException e) {
-                // even the processing of one value failed we continue with the next one (sometimes this is caused by
-                // buggy formats provided by the device
-                logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
-                        payload, e);
-            }
-        }
-
-        if (!updates.isEmpty()) {
-            int updated = 0;
-            for (Map.Entry<String, State> u : updates.entrySet()) {
-                String key = u.getKey();
-                updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
-            }
-            if (updated > 0) {
-                logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
-                if (profile.isSensor || profile.isRoller) {
-                    // CoAP is currently lacking the lastUpdate info, so we use host timestamp
-                    thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
-                }
-            }
-
-            if (profile.isLight && profile.inColor && col.isRgbValid()) {
-                // Update color picker from single values
-                if (col.isRgbValid()) {
-                    thingHandler.updateChannel(mkChannelId(CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_PICKER),
-                            col.toHSB(), false);
-                }
-            }
-
-            if ((profile.isRGBW2 && !profile.inColor) || profile.isRoller) {
-                // Aggregate Meter Data from different Coap updates
-                int i = 1;
-                double totalCurrent = 0.0;
-                double totalKWH = 0.0;
-                boolean updateMeter = false;
-                while (i <= thingHandler.getProfile().numMeters) {
-                    String meter = CHANNEL_GROUP_METER + i;
-                    double current = thingHandler.getChannelDouble(meter, CHANNEL_METER_CURRENTWATTS);
-                    double total = thingHandler.getChannelDouble(meter, CHANNEL_METER_TOTALKWH);
-                    totalCurrent += current >= 0 ? current : 0;
-                    totalKWH += total >= 0 ? total : 0;
-                    updateMeter |= current >= 0 | total >= 0;
-                    i++;
-                }
-                if (updateMeter) {
-                    thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_METER_CURRENTWATTS,
-                            toQuantityType(totalCurrent, DIGITS_WATT, Units.WATT));
-                    thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_LAST_UPDATE, getTimestamp());
-                }
-            }
-
-            // Old firmware release are lacking various status values, which are not updated using CoIoT.
-            // In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
-            // of the values are available
-            thingHandler.triggerUpdateFromCoap();
-        } else {
-            if (failed == sensorUpdates.size()) {
-                logger.debug("{}: Device description problem detected, re-discover", thingName);
-                coiotBound = false;
-                discover();
-            }
-        }
-
-        // Remember serial, new packets with same serial will be ignored
-        lastSerial = serial;
-        lastPayload = payload;
-    }
-
-    private void discover() {
-        if (coiot.getVersion() >= 2) {
-            {
-                try {
-                    // Try to device description using http request (FW 1.10+)
-                    String payload = api.getCoIoTDescription();
-                    if (!payload.isEmpty()) {
-                        logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName);
-                        handleDeviceDescription(thingName, payload);
-                        return;
-                    }
-                } catch (ShellyApiException e) {
-                    // ignore if not supported by device
-                }
-            }
-        }
-        reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
-    }
-
-    /**
-     * Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
-     * JsonSyntaxException
-     *
-     * @param json to be checked/fixed
-     */
-    private static String fixJSON(String payload) {
-        String json = payload;
-        json = json.replace("}{", "},{");
-        json = json.replace("][", "],[");
-        json = json.replace("],,[", "],[");
-        return json;
-    }
-
-    /**
-     * Send a new request (Discovery to get Device Description). Before a pending
-     * request will be canceled.
-     *
-     * @param request The current request (this will be canceled an a new one will
-     *            be created)
-     * @param ipAddress Device's IP address
-     * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
-     * @param con true: send as CON, false: send as NON
-     * @return new packet
-     */
-    private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
-        if ((request != null) && !request.isCanceled()) {
-            request.cancel();
-        }
-
-        resetSerial();
-        return newRequest(ipAddress, coiotPort, uri, con).send();
-    }
-
-    /**
-     * Allocate a new Request structure. A message observer will be added to get the
-     * callback when a response has been received.
-     *
-     * @param ipAddress IP address of the device
-     * @param uri URI to be addressed
-     * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
-     * @param con true: send as CON, false: send as NON
-     * @return new packet
-     */
-
-    private Request newRequest(String ipAddress, int port, String uri, Type con) {
-        // We need to build our own Request to set an empty Token
-        Request request = new Request(Code.GET, con);
-        request.setURI(completeUrl(ipAddress, port, uri));
-        request.setToken(EMPTY_BYTE);
-        request.addMessageObserver(new MessageObserverAdapter() {
-            @Override
-            public void onResponse(@Nullable Response response) {
-                processResponse(response);
-            }
-
-            @Override
-            public void onCancel() {
-                logger.debug("{}: CoAP Request was canceled", thingName);
-            }
-
-            @Override
-            public void onTimeout() {
-                logger.debug("{}: CoAP Request timed out", thingName);
-            }
-        });
-        return request;
-    }
-
-    /**
-     * Reset serial and payload used to detect duplicate messages, which have to be ignored.
-     * We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
-     * sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
-     * the payload.
-     */
-    private void resetSerial() {
-        lastSerial = -1;
-        lastPayload = "";
-    }
-
-    public int getVersion() {
-        return coiotVers;
-    }
-
-    /**
-     * Cancel pending requests and shutdown the client
-     */
-    public synchronized void stop() {
-        if (isStarted()) {
-            logger.debug("{}: Stopping CoAP Listener", thingName);
-            coapServer.stop(this);
-            CoapClient cclient = statusClient;
-            if (cclient != null) {
-                cclient.shutdown();
-                statusClient = null;
-            }
-            Request request = reqDescription;
-            if (!request.isCanceled()) {
-                request.cancel();
-            }
-            request = reqStatus;
-            if (!request.isCanceled()) {
-                request.cancel();
-            }
-        }
-        resetSerial();
-        coiotBound = false;
-    }
-
-    public long getMessageCount() {
-        return coiotMessages;
-    }
-
-    public long getErrorCount() {
-        return coiotErrors;
-    }
-
-    public void dispose() {
-        stop();
-    }
-
-    private static String completeUrl(String ipAddress, int port, String uri) {
-        return "coap://" + ipAddress + ":" + port + uri;
-    }
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapJSonDTO.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapJSonDTO.java
deleted file mode 100644 (file)
index af9834c..0000000
+++ /dev/null
@@ -1,339 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import com.google.gson.TypeAdapter;
-import com.google.gson.annotations.SerializedName;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonToken;
-import com.google.gson.stream.JsonWriter;
-
-/**
- * The {@link ShellyCoapJSonDTO} helps the CoIoT Json into Java objects
- *
- * @author Markus Michels - Initial contribution
- */
-public class ShellyCoapJSonDTO {
-    // Coap
-    public static final int COIOT_VERSION_1 = 1;
-    public static final int COIOT_VERSION_2 = 2;
-
-    public static final int COIOT_PORT = 5683;
-    public static final String COAP_MULTICAST_ADDRESS = "224.0.1.187";
-
-    public static final String COLOIT_URI_BASE = "/cit/";
-    public static final String COLOIT_URI_DEVDESC = COLOIT_URI_BASE + "d";
-    public static final String COLOIT_URI_DEVSTATUS = COLOIT_URI_BASE + "s";
-
-    public static final int COIOT_OPTION_GLOBAL_DEVID = 3332;
-    public static final int COIOT_OPTION_STATUS_VALIDITY = 3412;
-    public static final int COIOT_OPTION_STATUS_SERIAL = 3420;
-
-    public static final String COIOT_TAG_BLK = "blk";
-    public static final String COIOT_TAG_SEN = "sen";
-    public static final String COIOT_TAG_ACT = "act";
-    public static final String COIOT_TAG_GENERIC = "G";
-
-    public static class CoIotDescrBlk {
-        @SerializedName("I")
-        String id; // ID
-        @SerializedName("D")
-        String desc; // Description
-
-        // Sometimes sen entries are part of the blk array - not conforming the Spec!
-        @SerializedName("T")
-        public String type; // Type
-        @SerializedName("R")
-        public String range; // Range
-        @SerializedName("L")
-        public String links; // Links
-    }
-
-    public static class CoIotDescrSen {
-        @SerializedName("I")
-        String id; // ID
-        @SerializedName("D")
-        String desc = ""; // Description
-        @SerializedName("T")
-        public String type; // Type
-        @SerializedName("R")
-        public String range; // Range
-        @SerializedName("L")
-        public String links; // Links
-        @SerializedName("U")
-        public String unit; // Unit
-    }
-
-    public static class CoIotDescrP {
-        @SerializedName("I")
-        String id; // ID
-        @SerializedName("D")
-        String desc; // Description
-        @SerializedName("R")
-        public String range; // Range
-    }
-
-    public static class CoIotDescrAct {
-        @SerializedName("I")
-        String id; // ID
-        @SerializedName("D")
-        String desc; // Description
-        @SerializedName("L")
-        public String links; // Links
-        @SerializedName("P")
-        public List<CoIotDescrP> pTag; // ?
-    }
-
-    public static class CoIotDevDescription {
-        public List<CoIotDescrBlk> blk;
-        public List<CoIotDescrSen> sen;
-        // public List<CoIotDescrAct> act;
-
-        public CoIotDevDescription() {
-            blk = new ArrayList<>();
-            sen = new ArrayList<>();
-        }
-    }
-
-    public static class CoIotSensor {
-        @SerializedName("index")
-        public String id; // id
-        public double value; // value
-        public String valueStr; // value
-        public List<Object> valueArray;
-    }
-
-    public static class CoIotGenericSensorList {
-        @SerializedName("G")
-        public List<CoIotSensor> generic;
-
-        public CoIotGenericSensorList() {
-            generic = new ArrayList<>();
-        }
-    }
-
-    protected static class CoIotDevDescrTypeAdapter extends TypeAdapter<CoIotDevDescription> {
-        @Override
-        public CoIotDevDescription read(final JsonReader in) throws IOException {
-            CoIotDevDescription descr = new CoIotDevDescription();
-
-            /*
-             * parse JSON like
-             * "blk": [
-             * { "I": 0, "D": "Relay0"},
-             * { "I": 1, "D": "Sensors"} ],
-             * "sen": [
-             * { "I": 111, "T": "P", "D": "Power","R": "0/3500","L": 0},
-             * { "I": 112,"T": "S","D": "Switch","R": "0/1","L": 0}
-             * ]
-             */
-            in.beginObject();
-            String name = in.nextName();
-            if (name.equalsIgnoreCase(COIOT_TAG_BLK)) {
-                in.beginArray();
-                while (in.hasNext()) {
-                    CoIotDescrBlk blk = new CoIotDescrBlk();
-                    in.beginObject();
-                    while (in.hasNext()) {
-                        switch (in.nextName().toUpperCase()) {
-                            case "I":
-                                blk.id = in.nextString();
-                                break;
-                            case "D":
-                                blk.desc = in.nextString();
-                                break;
-                            default:
-                                // skip data
-                                in.nextNull();
-                        }
-                    }
-                    in.endObject();
-                    descr.blk.add(blk);
-                }
-                in.endArray();
-                name = in.nextName();
-            }
-
-            if (name.equalsIgnoreCase(COIOT_TAG_SEN)) {
-                /*
-                 * parse sensor list, e.g.
-                 * "sen":[
-                 * { "I":111,"T":"Red","R":"0/255","L":0},
-                 * { "I":121,"T":"Green","R":"0/255","L":0},
-                 * ]
-                 */
-                in.beginArray();
-                while (in.hasNext()) {
-                    CoIotDescrSen sen = new CoIotDescrSen();
-                    in.beginObject();
-                    while (in.hasNext()) {
-                        String tag = in.nextName();
-                        switch (tag.toUpperCase()) {
-                            case "I":
-                                sen.id = in.nextString();
-                                break;
-                            case "D":
-                                sen.desc = in.nextString();
-                                break;
-                            case "T":
-                                sen.type = in.nextString();
-                                break;
-                            case "R":
-                                JsonToken token = in.peek();
-                                if (token == JsonToken.BEGIN_ARRAY) {
-                                    // must be v2: an array
-                                    in.beginArray();
-                                    sen.range = "";
-                                    while (in.hasNext()) {
-                                        String value = in.nextString();
-                                        sen.range += sen.range.isEmpty() ? value : ";" + value;
-                                    }
-                                    in.endArray();
-                                } else {
-                                    sen.range = in.nextString();
-                                }
-                                break;
-                            case "L":
-                                sen.links = String.valueOf(in.nextInt());
-                                break;
-                            case "U": // New in CoAPv2: unit"
-                                sen.unit = in.nextString();
-                                break;
-                            default:
-                                // skip data
-                                in.nextNull();
-                        }
-                    }
-                    in.endObject();
-                    descr.sen.add(sen);
-                }
-
-                in.endArray();
-                if (in.hasNext()) {
-                    name = in.nextName();
-                }
-            }
-
-            if (name.equalsIgnoreCase(COIOT_TAG_ACT)) {
-                // skip record
-                in.skipValue();
-            }
-
-            in.endObject();
-            return descr;
-        }
-
-        @Override
-        public void write(final JsonWriter out, final CoIotDevDescription descr) throws IOException {
-            out.beginObject();
-            if (descr != null) {
-                out.name(COIOT_TAG_BLK).beginArray();
-                for (int i = 0; i < descr.blk.size(); i++) {
-                    CoIotDescrBlk blk = descr.blk.get(i);
-                    out.beginArray();
-                    out.value(blk.id);
-                    out.value(blk.desc);
-                    out.endArray();
-                }
-                out.endArray();
-
-                out.name(COIOT_TAG_SEN).beginArray();
-                for (int i = 0; i < descr.sen.size(); i++) {
-                    // Create element, e.g. {“I”:66, “D”:“lux”, “T”:“L”, “R”:“0/100000”, “L”:1},
-                    CoIotDescrSen sen = descr.sen.get(i);
-                    out.beginArray();
-                    out.value(sen.id);
-                    out.value(sen.desc);
-                    out.value(sen.type);
-                    out.value(sen.range);
-                    out.value(sen.links);
-                    if (sen.unit != null) {
-                        out.value(sen.unit);
-                    }
-                    out.endArray();
-                }
-                out.endArray();
-            }
-            out.endObject();
-        }
-    }
-
-    protected static class CoIotSensorTypeAdapter extends TypeAdapter<CoIotGenericSensorList> {
-        @Override
-        public CoIotGenericSensorList read(final JsonReader in) throws IOException {
-            CoIotGenericSensorList list = new CoIotGenericSensorList();
-
-            in.beginObject();
-            String generic = in.nextName();
-            if (generic.equals(COIOT_TAG_GENERIC)) {
-                in.beginArray();
-                while (in.hasNext()) {
-                    CoIotSensor sensor = new CoIotSensor();
-                    in.beginArray();
-                    in.nextInt(); // alway 0
-                    sensor.id = Integer.toString(in.nextInt());
-                    JsonToken token = in.peek();
-                    if (token == JsonToken.STRING) {
-                        // handle as string
-                        sensor.valueStr = in.nextString();
-                        sensor.value = -1;
-                    } else if (token == JsonToken.NUMBER) {
-                        // handle as double
-                        sensor.value = in.nextDouble();
-                        sensor.valueStr = "";
-                    } else if (token == JsonToken.BEGIN_ARRAY) {
-                        sensor.valueArray = new ArrayList<>();
-                        in.beginArray();
-                        while (in.hasNext()) {
-                            if (in.peek() == JsonToken.STRING) {
-                                sensor.valueArray.add(in.nextString());
-                            } else {
-                                // skip
-                                in.nextNull();
-                            }
-                        }
-                        in.endArray();
-                    }
-                    in.endArray();
-                    list.generic.add(sensor);
-                }
-                in.endArray();
-            }
-            in.endObject();
-
-            return list;
-        }
-
-        @Override
-        public void write(final JsonWriter out, final CoIotGenericSensorList o) throws IOException {
-            CoIotGenericSensorList sensors = o;
-            out.beginObject();
-            if (sensors != null) {
-                out.name(COIOT_TAG_GENERIC).beginArray();
-                for (int i = 0; i < sensors.generic.size(); i++) {
-                    out.beginArray();
-                    out.value(0);
-                    out.value(sensors.generic.get(i).id);
-                    out.value(sensors.generic.get(i).value);
-                    out.endArray();
-                }
-                out.endArray();
-            }
-            out.endObject();
-        }
-    }
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapListener.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapListener.java
deleted file mode 100644 (file)
index 5900580..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * The {@link ShellyCoapListener} describes the listening interface to process Coap responses
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public interface ShellyCoapListener {
-    public void processResponse(@Nullable Response response);
-}
diff --git a/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapServer.java b/bundles/org.openhab.binding.shelly/src/main/java/org/openhab/binding/shelly/internal/coap/ShellyCoapServer.java
deleted file mode 100644 (file)
index b7c525b..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.shelly.internal.coap;
-
-import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.COIOT_PORT;
-
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.eclipse.californium.core.CoapResource;
-import org.eclipse.californium.core.CoapServer;
-import org.eclipse.californium.core.coap.CoAP;
-import org.eclipse.californium.core.coap.CoAP.Code;
-import org.eclipse.californium.core.coap.CoAP.ResponseCode;
-import org.eclipse.californium.core.coap.Request;
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.californium.core.network.CoapEndpoint;
-import org.eclipse.californium.core.network.Exchange;
-import org.eclipse.californium.core.network.config.NetworkConfig;
-import org.eclipse.californium.elements.UdpMulticastConnector;
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link ShellyCoapServer} implements the UDP listener and status event processor (for /cit/s messages)
- *
- * @author Markus Michels - Initial contribution
- */
-@NonNullByDefault
-public class ShellyCoapServer {
-    private final Logger logger = LoggerFactory.getLogger(ShellyCoapServer.class);
-
-    boolean started = false;
-    private CoapEndpoint statusEndpoint = new CoapEndpoint.Builder().build();
-    private @Nullable UdpMulticastConnector statusConnector;
-    private CoapServer server = new CoapServer(NetworkConfig.getStandard(), COIOT_PORT);
-    private final Set<ShellyCoapListener> coapListeners = ConcurrentHashMap.newKeySet();
-
-    protected class ShellyStatusListener extends CoapResource {
-        private ShellyCoapServer listener;
-
-        public ShellyStatusListener(String uri, ShellyCoapServer listener) {
-            super(uri, true);
-            getAttributes().setTitle("ShellyCoapListener");
-            this.listener = listener;
-        }
-
-        @Override
-        public void handleRequest(@Nullable final Exchange exchange) {
-            if (exchange != null) {
-                Request request = exchange.getRequest();
-                Code code = exchange.getRequest().getCode();
-                switch (code) {
-                    case CUSTOM_30:
-                    case PUT: // Shelly Motion beta: incorrect, but handle the format
-                        listener.processResponse(createResponse(request));
-                        break;
-                    default:
-                        super.handleRequest(exchange);
-                }
-            }
-        }
-    }
-
-    public synchronized void start(String localIp, int port, ShellyCoapListener listener)
-            throws UnknownHostException, SocketException {
-        if (!started) {
-            logger.debug("Initializing CoIoT listener (local IP={}:{})", localIp, port);
-            NetworkConfig nc = NetworkConfig.getStandard();
-            InetAddress localAddr = InetAddress.getByName(localIp);
-            InetSocketAddress localPort = new InetSocketAddress(port);
-
-            // Join the multicast group on the selected network interface
-            statusConnector = new UdpMulticastConnector(localAddr, localPort, CoAP.MULTICAST_IPV4); // bind UDP listener
-            statusEndpoint = new CoapEndpoint.Builder().setNetworkConfig(nc).setConnector(statusConnector).build();
-            server = new CoapServer(NetworkConfig.getStandard(), port);
-            server.addEndpoint(statusEndpoint);
-            CoapResource cit = new ShellyStatusListener("cit", this);
-            CoapResource s = new ShellyStatusListener("s", this);
-            cit.add(s);
-            server.add(cit);
-            started = true;
-        }
-
-        if (!coapListeners.contains(listener)) {
-            coapListeners.add(listener);
-        }
-    }
-
-    protected void processResponse(Response response) {
-        coapListeners.forEach(listener -> listener.processResponse(response));
-    }
-
-    public static Response createResponse(Request request) {
-        Response response = Response.createResponse(request, ResponseCode.CONTENT);
-        response.setType(request.getType());
-        response.setSourceContext(request.getSourceContext());
-        response.setMID(request.getMID());
-        response.setOptions(request.getOptions());
-        response.setPayload(request.getPayload());
-        return response;
-    }
-
-    @Nullable
-    public CoapEndpoint getEndpoint() {
-        return statusEndpoint;
-    }
-
-    /**
-     * Cancel pending requests and shutdown the client
-     */
-    public void stop(ShellyCoapListener listener) {
-        coapListeners.remove(listener);
-        if (coapListeners.isEmpty()) {
-            stop();
-        }
-    }
-
-    private synchronized void stop() {
-        if (started) {
-            // Last listener
-            server.stop();
-            statusEndpoint.stop();
-            coapListeners.clear();
-            started = false;
-            logger.debug("CoAP Listener stopped");
-        }
-    }
-
-    public void dispose() {
-        stop();
-    }
-}
index c185d34d93ff93822da9bbb070469bd56c22fcf2..cf38e9ed839697c373d5ff6a90fc2e7216eb99a1 100755 (executable)
@@ -29,7 +29,7 @@ import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
@@ -140,7 +140,7 @@ public class ShellyDiscoveryParticipant implements MDNSDiscoveryParticipant {
             config.password = bindingConfig.defaultPassword;
 
             try {
-                ShellyHttpApi api = new ShellyHttpApi(name, config, httpClient);
+                Shelly1HttpApi api = new Shelly1HttpApi(name, config, httpClient);
 
                 profile = api.getDeviceProfile(thingType);
                 logger.debug("{}: Shelly settings : {}", name, profile.settingsJson);
index 924d6b847509bb184499d997ed63befb98bcbf10..61b5b437ee2ed67502539508927ea7f00963cf21 100644 (file)
@@ -12,8 +12,8 @@
  */
 package org.openhab.binding.shelly.internal.discovery;
 
-import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.ShellyBindingConstants.BINDING_ID;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import java.util.LinkedHashMap;
@@ -30,6 +30,219 @@ import org.openhab.core.thing.ThingUID;
  */
 @NonNullByDefault
 public class ShellyThingCreator {
+    // Device Types
+    public static final String SHELLYDT_1 = "SHSW-1";
+    public static final String SHELLYDT_1PM = "SHSW-PM";
+    public static final String SHELLYDT_1L = "SHSW-L";
+    public static final String SHELLYDT_SHPLG = "SHPLG-1";
+    public static final String SHELLYDT_SHPLG_S = "SHPLG-S";
+    public static final String SHELLYDT_SHPLG_U1 = "SHPLG-U1";
+    public static final String SHELLYDT_SHELLY2 = "SHSW-21";
+    public static final String SHELLYDT_SHELLY25 = "SHSW-25";
+    public static final String SHELLYDT_SHPRO = "SHSW-44";
+    public static final String SHELLYDT_EM = "SHEM";
+    public static final String SHELLYDT_3EM = "SHEM-3";
+    public static final String SHELLYDT_HT = "SHHT-1";
+    public static final String SHELLYDT_DW = "SHDW-1";
+    public static final String SHELLYDT_DW2 = "SHDW-2";
+    public static final String SHELLYDT_SENSE = "SHSEN-1";
+    public static final String SHELLYDT_MOTION = "SHMOS-01";
+    public static final String SHELLYDT_MOTION2 = "SHMOS-02";
+    public static final String SHELLYDT_GAS = "SHGS-1";
+    public static final String SHELLYDT_DIMMER = "SHDM-1";
+    public static final String SHELLYDT_DIMMER2 = "SHDM-2";
+    public static final String SHELLYDT_IX3 = "SHIX3-1";
+    public static final String SHELLYDT_BULB = "SHBLB-1";
+    public static final String SHELLYDT_DUO = "SHBDUO-1";
+    public static final String SHELLYDT_DUORGBW = "SHCB-1";
+    public static final String SHELLYDT_VINTAGE = "SHVIN-1";
+    public static final String SHELLYDT_RGBW2 = "SHRGBW2";
+    public static final String SHELLYDT_BUTTON1 = "SHBTN-1";
+    public static final String SHELLYDT_BUTTON2 = "SHBTN-2";
+    public static final String SHELLYDT_UNI = "SHUNI-1";
+    public static final String SHELLYDT_TRV = "SHTRV-01";
+
+    // Shelly Plus Series
+    public static final String SHELLYDT_PLUS1 = "SNSW-001X16EU";
+    public static final String SHELLYDT_PLUS1PM = "SNSW-001P16EU";
+    public static final String SHELLYDT_PLUS1UL = "SNSW-001X15UL";
+    public static final String SHELLYDT_PLUS1PMUL = "SNSW-001P15UL";
+    public static final String SHELLYDT_PLUS2PM_RELAY = "SNSW-002P16EU-relay";
+    public static final String SHELLYDT_PLUS2PM_ROLLER = "SNSW-002P16EU-roller";
+    public static final String SHELLYDT_PLUSPLUGUS = "SNPL-00116US";
+    public static final String SHELLYDT_PLUSI4 = "SNSN-0024X";
+    public static final String SHELLYDT_PLUSI4DC = "SNSN-0D24X";
+    public static final String SHELLYDT_PLUSHT = "SNSN-0013A";
+
+    // Shelly Pro Series
+    public static final String SHELLYDT_PRO1 = "SPSW-001XE16EU";
+    public static final String SHELLYDT_PRO1_2 = "SPSW-101XE16EU";
+    public static final String SHELLYDT_PRO1PM = "SPSW-001PE16EU";
+    public static final String SHELLYDT_PRO1PM_ = "SPSW-201PE16EU";
+    public static final String SHELLYDT_PRO1PM_2 = "SPSW-101PE16EU";
+    public static final String SHELLYDT_PRO2_RELAY = "SPSW-002XE16EU-relay";
+    public static final String SHELLYDT_PRO2_ROLLER = "SPSW-002XE16EU-roller";
+    public static final String SHELLYDT_PRO2_RELAY_2 = "SPSW-102XE16EU-relay";
+    public static final String SHELLYDT_PRO2_ROLLER_2 = "SPSW-102XE16EU-roller";
+    public static final String SHELLYDT_PRO2PM_RELAY = "SPSW-002PE16EU-relay";
+    public static final String SHELLYDT_PRO2PM_ROLLER = "SPSW-002PE16EU-roller";
+    public static final String SHELLYDT_PRO2PM_RELAY_2 = "SPSW-002PE16EU-relay";
+    public static final String SHELLYDT_PRO2PM_ROLLER_2 = "SPSW-002PE16EU-roller";
+    public static final String SHELLYDT_PRO3 = "SPSW-003XE16EU";
+    public static final String SHELLYDT_PRO4PM = "SPSW-004PE16EU";
+    public static final String SHELLYDT_PRO4PM_2 = "SPSW-104PE16EU";
+
+    // Thing names
+    public static final String THING_TYPE_SHELLY1_STR = "shelly1";
+    public static final String THING_TYPE_SHELLY1L_STR = "shelly1l";
+    public static final String THING_TYPE_SHELLY1PM_STR = "shelly1pm";
+    public static final String THING_TYPE_SHELLYEM_STR = "shellyem";
+    public static final String THING_TYPE_SHELLY3EM_STR = "shellyem3"; // bad: misspelled product name, it's 3EM
+    public static final String THING_TYPE_SHELLY2_PREFIX = "shellyswitch";
+    public static final String THING_TYPE_SHELLY2_RELAY_STR = "shelly2-relay";
+    public static final String THING_TYPE_SHELLY2_ROLLER_STR = "shelly2-roller";
+    public static final String THING_TYPE_SHELLY25_PREFIX = "shellyswitch25";
+    public static final String THING_TYPE_SHELLY25_RELAY_STR = "shelly25-relay";
+    public static final String THING_TYPE_SHELLY25_ROLLER_STR = "shelly25-roller";
+    public static final String THING_TYPE_SHELLY4PRO_STR = "shelly4pro";
+    public static final String THING_TYPE_SHELLYPLUG_STR = "shellyplug";
+    public static final String THING_TYPE_SHELLYPLUGS_STR = "shellyplugs";
+    public static final String THING_TYPE_SHELLYPLUGU1_STR = "shellyplugu1"; // Shely Plug US
+    public static final String THING_TYPE_SHELLYDIMMER_STR = "shellydimmer";
+    public static final String THING_TYPE_SHELLYDIMMER2_STR = "shellydimmer2";
+    public static final String THING_TYPE_SHELLYIX3_STR = "shellyix3";
+    public static final String THING_TYPE_SHELLYBULB_STR = "shellybulb";
+    public static final String THING_TYPE_SHELLYDUO_STR = "shellybulbduo";
+    public static final String THING_TYPE_SHELLYVINTAGE_STR = "shellyvintage";
+    public static final String THING_TYPE_SHELLYRGBW2_PREFIX = "shellyrgbw2";
+    public static final String THING_TYPE_SHELLYRGBW2_COLOR_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-color";
+    public static final String THING_TYPE_SHELLYRGBW2_WHITE_STR = THING_TYPE_SHELLYRGBW2_PREFIX + "-white";
+    public static final String THING_TYPE_SHELLYDUORGBW_STR = "shellycolorbulb";
+    public static final String THING_TYPE_SHELLYHT_STR = "shellyht";
+    public static final String THING_TYPE_SHELLYSMOKE_STR = "shellysmoke";
+    public static final String THING_TYPE_SHELLYGAS_STR = "shellygas";
+    public static final String THING_TYPE_SHELLYFLOOD_STR = "shellyflood";
+    public static final String THING_TYPE_SHELLYDOORWIN_STR = "shellydw";
+    public static final String THING_TYPE_SHELLYDOORWIN2_STR = "shellydw2";
+    public static final String THING_TYPE_SHELLYEYE_STR = "shellyseye";
+    public static final String THING_TYPE_SHELLYSENSE_STR = "shellysense";
+    public static final String THING_TYPE_SHELLYTRV_STR = "shellytrv";
+    public static final String THING_TYPE_SHELLYMOTION_STR = "shellymotion";
+    public static final String THING_TYPE_SHELLYMOTION2_STR = "shellymotion2";
+    public static final String THING_TYPE_SHELLYBUTTON1_STR = "shellybutton1";
+    public static final String THING_TYPE_SHELLYBUTTON2_STR = "shellybutton2";
+    public static final String THING_TYPE_SHELLYUNI_STR = "shellyuni";
+
+    // Shelly Plus Seriens
+    public static final String THING_TYPE_SHELLYPLUS1_STR = "shellyplus1";
+    public static final String THING_TYPE_SHELLYPLUS1PM_STR = "shellyplus1pm";
+    public static final String THING_TYPE_SHELLYPLUS2PM_RELAY_STR = "shellyplus2pm-relay";
+    public static final String THING_TYPE_SHELLYPLUS2PM_ROLLER_STR = "shellyplus2pm-roller";
+    public static final String THING_TYPE_SHELLYPLUSI4_STR = "shellyplusi4";
+    public static final String THING_TYPE_SHELLYPLUSI4DC_STR = "shellyplusi4dc";
+    public static final String THING_TYPE_SHELLYPLUSHT_STR = "shellyplusht";
+    public static final String THING_TYPE_SHELLYPLUSPLUGUS_STR = "shellyplusplugus";
+
+    // Shelly Pro Series
+    public static final String THING_TYPE_SHELLYPRO1_STR = "shellypro1";
+    public static final String THING_TYPE_SHELLYPRO1PM_STR = "shellypro1pm";
+    public static final String THING_TYPE_SHELLYPRO2_RELAY_STR = "shellypro2-relay";
+    public static final String THING_TYPE_SHELLYPRO2_ROLLER_STR = "shellypro2-roller";
+    public static final String THING_TYPE_SHELLYPRO2PM_RELAY_STR = "shellypro2pm-relay";
+    public static final String THING_TYPE_SHELLYPRO2PM_ROLLER_STR = "shellypro2pm-roller";
+    public static final String THING_TYPE_SHELLYPRO3_STR = "shellypro3";
+    public static final String THING_TYPE_SHELLYPRO4PM_STR = "shellypro4pm";
+
+    public static final String THING_TYPE_SHELLYPROTECTED_STR = "shellydevice";
+    public static final String THING_TYPE_SHELLYUNKNOWN_STR = "shellyunknown";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_SHELLY1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY1L = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1L_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY1PM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY1PM_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYEM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEM_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY3EM = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY3EM_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY2_RELAY = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLY2_RELAY_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY2_ROLLER = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLY2_ROLLER_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY25_RELAY = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLY25_RELAY_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY25_ROLLER = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLY25_ROLLER_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLY4PRO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLY4PRO_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUG = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUG_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUGS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUGS_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUGU1 = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUGU1_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYUNI = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYUNI_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYDIMMER = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYDIMMER_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYDIMMER2 = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYDIMMER2_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYIX3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYIX3_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYBULB = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYBULB_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYDUO = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYDUO_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYVINTAGE = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYVINTAGE_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYDUORGBW = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYDUORGBW_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYHT = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYHT_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYSENSE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSENSE_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYSMOKE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYSMOKE_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYGAS = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYGAS_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYFLOOD = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYFLOOD_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYDOORWIN_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYDOORWIN2 = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYDOORWIN2_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYTRV = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYTRV_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYBUTTON1 = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYBUTTON1_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYBUTTON2 = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYBUTTON2_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYEYE = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYEYE_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLMOTION = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYMOTION_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_COLOR = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYRGBW2_COLOR_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYRGBW2_WHITE = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYRGBW2_WHITE_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPROTECTED = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPROTECTED_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYUNKNOWN = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYUNKNOWN_STR);
+
+    // Shelly Plus/Pro
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUS1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPLUS1_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUS1PM = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUS1PM_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUS2PM_RELAY = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUS2PM_ROLLER = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUSI4 = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUSI4_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUSI4DC = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUSI4DC_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUSHT = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUSHT_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPLUSPLUGUS = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPLUSPLUGUS_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO1 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPRO1_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO1PM = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPRO1PM_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO2_RELAY = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPRO2_RELAY_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO2_ROLLER = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPRO2_ROLLER_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_RELAY = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPRO2PM_RELAY_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO2PM_ROLLER = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO3 = new ThingTypeUID(BINDING_ID, THING_TYPE_SHELLYPRO3_STR);
+    public static final ThingTypeUID THING_TYPE_SHELLYPRO4PM = new ThingTypeUID(BINDING_ID,
+            THING_TYPE_SHELLYPRO4PM_STR);
+
     private static final Map<String, String> THING_TYPE_MAPPING = new LinkedHashMap<>();
     static {
         // mapping by device type id
@@ -57,6 +270,35 @@ public class ShellyThingCreator {
         THING_TYPE_MAPPING.put(SHELLYDT_TRV, THING_TYPE_SHELLYTRV_STR);
         THING_TYPE_MAPPING.put(SHELLYDT_MOTION, THING_TYPE_SHELLYMOTION_STR);
 
+        // Plus Series
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUS1, THING_TYPE_SHELLYPLUS1_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUS1PM, THING_TYPE_SHELLYPLUS1PM_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUS1UL, THING_TYPE_SHELLYPLUS1_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUS1PMUL, THING_TYPE_SHELLYPLUS1PM_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_RELAY, THING_TYPE_SHELLYPLUS2PM_RELAY_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUS2PM_ROLLER, THING_TYPE_SHELLYPLUS2PM_ROLLER_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUSPLUGUS, THING_TYPE_SHELLYPLUSPLUGUS_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4DC, THING_TYPE_SHELLYPLUSI4DC_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUSI4, THING_TYPE_SHELLYPLUSI4_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PLUSHT, THING_TYPE_SHELLYPLUSHT_STR);
+
+        // Pro Series
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO1, THING_TYPE_SHELLYPRO1_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO1_2, THING_TYPE_SHELLYPRO1_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM, THING_TYPE_SHELLYPRO1PM_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO1PM_2, THING_TYPE_SHELLYPRO1PM_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY, THING_TYPE_SHELLYPRO2_RELAY_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2_RELAY_2, THING_TYPE_SHELLYPRO2_RELAY_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER, THING_TYPE_SHELLYPRO2_ROLLER_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2_ROLLER_2, THING_TYPE_SHELLYPRO2_ROLLER_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_RELAY_2, THING_TYPE_SHELLYPRO2PM_RELAY_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO2PM_ROLLER_2, THING_TYPE_SHELLYPRO2PM_ROLLER_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO3, THING_TYPE_SHELLYPRO3_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM, THING_TYPE_SHELLYPRO4PM_STR);
+        THING_TYPE_MAPPING.put(SHELLYDT_PRO4PM_2, THING_TYPE_SHELLYPRO4PM_STR);
+
         // 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 f0a4ddd752aa970f42fd9cf1ca1e6802906feb79..4c8e0d3705051e92714f8b5e9c4f187c1b8f23ba 100755 (executable)
@@ -13,7 +13,8 @@
 package org.openhab.binding.shelly.internal.handler;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
 import static org.openhab.binding.shelly.internal.handler.ShellyComponents.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 import static org.openhab.core.thing.Thing.*;
@@ -31,17 +32,17 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDevice;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapHandler;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyInputState;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDevice;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapHandler;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.discovery.ShellyThingCreator;
@@ -84,11 +85,13 @@ public class ShellyBaseHandler extends BaseThingHandler
     public String thingType = "";
 
     protected final ShellyApiInterface api;
+    private final HttpClient httpClient;
+
     protected ShellyBindingConfiguration bindingConfig;
     protected ShellyThingConfiguration config = new ShellyThingConfiguration();
     protected ShellyDeviceProfile profile = new ShellyDeviceProfile(); // init empty profile to avoid NPE
     protected ShellyDeviceStats stats = new ShellyDeviceStats();
-    private final ShellyCoapHandler coap;
+    private final Shelly1CoapHandler coap;
     public boolean autoCoIoT = false;
 
     public final ShellyTranslationProvider messages;
@@ -124,7 +127,7 @@ public class ShellyBaseHandler extends BaseThingHandler
      * @param httpPort from httpService
      */
     public ShellyBaseHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
-            final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+            final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
             int httpPort, final HttpClient httpClient) {
         super(thing);
 
@@ -135,11 +138,12 @@ public class ShellyBaseHandler extends BaseThingHandler
         this.bindingConfig = bindingConfig;
         this.config = getConfigAs(ShellyThingConfiguration.class);
 
+        this.httpClient = httpClient;
         this.localIP = localIP;
         this.localPort = String.valueOf(httpPort);
-        this.api = new ShellyHttpApi(thingName, config, httpClient);
+        this.api = new Shelly1HttpApi(thingName, config, httpClient);
 
-        coap = new ShellyCoapHandler(this, coapServer);
+        coap = new Shelly1CoapHandler(this, coapServer);
     }
 
     @Override
@@ -196,6 +200,11 @@ public class ShellyBaseHandler extends BaseThingHandler
         return config;
     }
 
+    @Override
+    public HttpClient getHttpClient() {
+        return httpClient;
+    }
+
     /**
      * This routine is called every time the Thing configuration has been changed
      */
@@ -274,7 +283,7 @@ public class ShellyBaseHandler extends BaseThingHandler
 
         if (config.eventsCoIoT && (tmpPrf.settings.coiot != null) && (tmpPrf.settings.coiot.enabled != null)) {
             String devpeer = getString(tmpPrf.settings.coiot.peer);
-            String ourpeer = config.localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT;
+            String ourpeer = config.localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT;
             if (!tmpPrf.settings.coiot.enabled || (profile.isMotion && devpeer.isEmpty())) {
                 try {
                     api.setCoIoTPeer(ourpeer);
@@ -749,7 +758,7 @@ public class ShellyBaseHandler extends BaseThingHandler
                         if (isButton) {
                             triggerButton(group, idx, mapButtonEvent(event));
                             channel = CHANNEL_BUTTON_TRIGGER + profile.getInputSuffix(idx);
-                            payload = ShellyApiJsonDTO.mapButtonEvent(event);
+                            payload = Shelly1ApiJsonDTO.mapButtonEvent(event);
                         } else {
                             logger.debug("{}: Relay button is not in memontary or detached mode, ignore SHORT/LONGPUSH",
                                     thingName);
index 80851e5c224c61904633cc504a323ac8d82bd256..d424edbd6601e93f78768c072702b7f7f09ba104 100644 (file)
@@ -12,7 +12,7 @@
  */
 package org.openhab.binding.shelly.internal.handler;
 
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 
 import java.math.BigDecimal;
 
index 9d68b0b91ab831139ed2776b23e9f23fe0f3fa64..34f94f9ff0d22f98fae4231b13ab0fb93d9f6990 100644 (file)
 package org.openhab.binding.shelly.internal.handler;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor.ShellyADC;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyThermnostat;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
+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.ShellyADC;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyThermnostat;
 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.OpenClosedType;
index 54f722cb7117ce36d302c7a1cde756a11a20785f..ea506aa0a175d15a2c61e53027fe6b3f6df389b7 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.shelly.internal.handler;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import java.util.Map;
@@ -22,13 +22,13 @@ import java.util.TreeMap;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLightChannel;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLightChannel;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -66,7 +66,7 @@ public class ShellyLightHandler extends ShellyBaseHandler {
      * @param httpPort port of the openHAB HTTP API
      */
     public ShellyLightHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
-            final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+            final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
             int httpPort, final HttpClient httpClient) {
         super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
         channelColors = new TreeMap<>();
index c3f5ea008f9cd7a1838d700ad8dade00f5b1ff6f..f941638c5f521b3d2872975926e8598fb80f46b7 100644 (file)
@@ -14,7 +14,7 @@ package org.openhab.binding.shelly.internal.handler;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
 import org.openhab.core.thing.Thing;
@@ -36,7 +36,7 @@ public class ShellyProtectedHandler extends ShellyBaseHandler {
      * @param httpPort port of the openHAB HTTP API
      */
     public ShellyProtectedHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
-            final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+            final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
             int httpPort, final HttpClient httpClient) {
         super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
     }
index 8fbe6210e7570e191472d50aaab8450e95334043..34fbb7c9a7be75296f943ffe5207fca72baee816 100644 (file)
 package org.openhab.binding.shelly.internal.handler;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.*;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRoller;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapServer;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.provider.ShellyChannelDefinitions;
 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -69,7 +69,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
      * @param httpPort port of the openHAB HTTP API
      */
     public ShellyRelayHandler(final Thing thing, final ShellyTranslationProvider translationProvider,
-            final ShellyBindingConfiguration bindingConfig, final ShellyCoapServer coapServer, final String localIP,
+            final ShellyBindingConfiguration bindingConfig, final Shelly1CoapServer coapServer, final String localIP,
             int httpPort, final HttpClient httpClient) {
         super(thing, translationProvider, bindingConfig, coapServer, localIP, httpPort, httpClient);
     }
@@ -222,7 +222,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
         int position = -1;
 
         if ((command instanceof UpDownType) || (command instanceof OnOffType)) {
-            ShellyControlRoller rstatus = api.getRollerStatus(index);
+            ShellyRollerStatus rstatus = api.getRollerStatus(index);
 
             if (!getString(rstatus.state).isEmpty() && !getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_STOP)) {
                 if ((command == UpDownType.UP && getString(rstatus.state).equals(SHELLY_ALWD_ROLLER_TURN_OPEN))
@@ -317,7 +317,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
         }
     }
 
-    private void createRollerChannels(ShellyControlRoller roller) {
+    private void createRollerChannels(ShellyRollerStatus roller) {
         if (!areChannelsCreated()) {
             updateChannelDefinitions(ShellyChannelDefinitions.createRollerChannels(getThing(), roller));
         }
@@ -406,7 +406,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
 
             for (ShellySettingsRoller roller : status.rollers) {
                 if (roller.isValid) {
-                    ShellyControlRoller control = api.getRollerStatus(i);
+                    ShellyRollerStatus control = api.getRollerStatus(i);
                     Integer relayIndex = i + 1;
                     String groupName = profile.numRollers > 1 ? CHANNEL_GROUP_ROL_CONTROL + relayIndex.toString()
                             : CHANNEL_GROUP_ROL_CONTROL;
@@ -456,7 +456,7 @@ public class ShellyRelayHandler extends ShellyBaseHandler {
             // the same structure as lights[] from Bulb,RGBW2 and Duo. The tag gets replaced by dimmers[] so that Gson
             // maps to a different structure (ShellyShortLight).
             Gson gson = new Gson();
-            ShellySettingsStatus dstatus = fromJson(gson, ShellyApiJsonDTO.fixDimmerJson(orgStatus.json),
+            ShellySettingsStatus dstatus = fromJson(gson, Shelly1ApiJsonDTO.fixDimmerJson(orgStatus.json),
                     ShellySettingsStatus.class);
 
             logger.trace("{}: Updating {} dimmers(s)", thingName, dstatus.dimmers.size());
index 37a43c780b57dfc7adc6446546fbd2d3a163ef5c..0adb1f040290c5d7cc2f42ddab3ed8d4f8f5ae34 100644 (file)
@@ -17,10 +17,11 @@ import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.core.thing.Channel;
 import org.openhab.core.thing.Thing;
@@ -81,6 +82,8 @@ public interface ShellyThingInterface {
 
     public ShellyThingConfiguration getThingConfig();
 
+    public HttpClient getHttpClient();
+
     public String getProperty(String key);
 
     public void updateProperties(String key, String value);
index e13df68b42c050626f37d0cde92b17ab4b66c57c..4c3e14c0956b4b1e7272b1cc22c528a19dbf1652 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.shelly.internal.manager;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.PROPERTY_SERVICE_NAME;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.SHELLY_COIOT_MCAST;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_COIOT_MCAST;
 import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
@@ -27,11 +27,11 @@ import org.eclipse.jetty.http.HttpStatus;
 import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api.ShellyApiInterface;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyOtaCheckResult;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsLogin;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
-import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyOtaCheckResult;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsLogin;
+import org.openhab.binding.shelly.internal.api1.Shelly1CoapJSonDTO;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -83,7 +83,7 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
             ShellyThingConfiguration config = getThingConfig(th, properties);
             ShellyDeviceProfile profile = th.getProfile();
             ShellyApiInterface api = th.getApi();
-            new ShellyHttpApi(uid, config, httpClient);
+            new Shelly1HttpApi(uid, config, httpClient);
 
             int refreshTimer = 0;
             switch (action) {
@@ -139,7 +139,7 @@ public class ShellyManagerActionPage extends ShellyManagerPage {
 
                     String peer = getString(profile.settings.coiot.peer);
                     boolean mcast = peer.isEmpty() || SHELLY_COIOT_MCAST.equalsIgnoreCase(peer);
-                    String newPeer = mcast ? localIp + ":" + ShellyCoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
+                    String newPeer = mcast ? localIp + ":" + Shelly1CoapJSonDTO.COIOT_PORT : SHELLY_COIOT_MCAST;
                     String displayPeer = mcast ? newPeer : "Multicast";
 
                     if (profile.isMotion && action.equalsIgnoreCase(ACTION_SETCOIOT_MCAST)) {
index 46949809b93f56a844f31650d105232f6a86269e..ab498d967edf840658038d90a5ffe5132e84cc97 100644 (file)
@@ -33,9 +33,9 @@ import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
 import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsUpdate;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsUpdate;
+import org.openhab.binding.shelly.internal.api1.Shelly1HttpApi;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyManagerInterface;
 import org.openhab.binding.shelly.internal.provider.ShellyTranslationProvider;
@@ -122,7 +122,7 @@ public class ShellyManagerOtaPage extends ShellyManagerPage {
 
                 new Thread(() -> { // schedule asynchronous reboot
                     try {
-                        ShellyHttpApi api = new ShellyHttpApi(uid, config, httpClient);
+                        Shelly1HttpApi api = new Shelly1HttpApi(uid, config, httpClient);
                         ShellySettingsUpdate result = api.firmwareUpdate(updateUrl);
                         String status = getString(result.status);
                         logger.info("{}: {}", th.getThingName(), getMessage("fwupdate.initiated", status));
index 0d17c0918950ec6906a58045d1c8fec7746a456f..a1fad7dc33863e75f08a0484b7b70ea668e5aefa 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.shelly.internal.manager;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
+import static org.openhab.binding.shelly.internal.discovery.ShellyThingCreator.*;
 import static org.openhab.binding.shelly.internal.manager.ShellyManagerConstants.*;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 import static org.openhab.core.thing.Thing.*;
@@ -44,7 +45,7 @@ import org.openhab.binding.shelly.internal.ShellyHandlerFactory;
 import org.openhab.binding.shelly.internal.api.ShellyApiException;
 import org.openhab.binding.shelly.internal.api.ShellyApiResult;
 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
-import org.openhab.binding.shelly.internal.api.ShellyHttpApi;
+import org.openhab.binding.shelly.internal.api.ShellyHttpClient;
 import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
 import org.openhab.binding.shelly.internal.handler.ShellyDeviceStats;
@@ -491,7 +492,7 @@ public class ShellyManagerPage {
         try {
             Request request = httpClient.newRequest(url).method(method).timeout(SHELLY_API_TIMEOUT_MS,
                     TimeUnit.MILLISECONDS);
-            request.header(HttpHeader.ACCEPT, ShellyHttpApi.CONTENT_TYPE_JSON);
+            request.header(HttpHeader.ACCEPT, ShellyHttpClient.CONTENT_TYPE_JSON);
             logger.trace("{}: HTTP {} {}", LOG_PREFIX, method, url);
             ContentResponse contentResponse = request.send();
             apiResult = new ShellyApiResult(contentResponse);
index 2ac54c81e8b491138151e8a5e66b5b8fc96dd6c5..932046f44b968b8467719ad5811a2a69c1a997e0 100644 (file)
@@ -72,14 +72,14 @@ public class ShellyManagerServlet extends HttpServlet {
         className = substringAfterLast(getClass().toString(), ".");
         this.httpService = httpService;
         String localIp = getString(networkAddressService.getPrimaryIpv4HostAddress());
-        int localPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
+        Integer localPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
         this.manager = new ShellyManager(configurationAdmin, translationProvider,
                 httpClientFactory.getCommonHttpClient(), localIp, localPort, handlerFactory);
 
         try {
             httpService.registerServlet(SERVLET_URI, this, null, httpService.createDefaultHttpContext());
             // Promote Shelly Manager usage
-            logger.info("{}", translationProvider.get("status.managerstarted", localIp, localPort + ""));
+            logger.info("{}", translationProvider.get("status.managerstarted", localIp, localPort.toString()));
         } catch (NamespaceException | ServletException | IllegalArgumentException e) {
             logger.warn("{}: Unable to initialize bindingConfig", className, e);
         }
index 53f6c85508ea860d0e0d477dd5f132b36c7241aa..670d953a5ea3e5b3a567ebd2407ab9091df4c243 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.shelly.internal.provider;
 
 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
-import static org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.SHELLY_API_INVTEMP;
+import static org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.SHELLY_API_INVTEMP;
 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
 
 import java.util.HashMap;
@@ -27,21 +27,21 @@ import javax.measure.Unit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyControlRoller;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyInputState;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsDimmer;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsEMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsGlobal;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsMeter;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsRgbwLight;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellySettingsStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortLightStatus;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyShortStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusLightChannel;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusRelay;
-import org.openhab.binding.shelly.internal.api.ShellyApiJsonDTO.ShellyStatusSensor;
 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.ShellyRollerStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsDimmer;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsEMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsGlobal;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsMeter;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsRgbwLight;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellySettingsStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortLightStatus;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyShortStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusLightChannel;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusRelay;
+import org.openhab.binding.shelly.internal.api1.Shelly1ApiJsonDTO.ShellyStatusSensor;
 import org.openhab.binding.shelly.internal.handler.ShellyThingInterface;
 import org.openhab.core.thing.Channel;
 import org.openhab.core.thing.ChannelUID;
@@ -262,7 +262,7 @@ public class ShellyChannelDefinitions {
 
         addChannel(thing, add, profile.settings.name != null, CHGR_DEVST, CHANNEL_DEVST_NAME);
 
-        if (!profile.isSensor && !profile.isIX3 && getDouble(status.temperature) != SHELLY_API_INVTEMP) {
+        if (!profile.isSensor && !profile.isIX && getDouble(status.temperature) != SHELLY_API_INVTEMP) {
             // Only some devices report the internal device temp
             addChannel(thing, add, status.tmp != null || status.temperature != null, CHGR_DEVST, CHANNEL_DEVST_ITEMP);
         }
@@ -298,15 +298,18 @@ public class ShellyChannelDefinitions {
         Map<String, Channel> add = new LinkedHashMap<>();
         String group = profile.getControlGroup(idx);
 
-        ShellySettingsRelay rs = profile.settings.relays.get(idx);
-        ShellyShortStatusRelay rstatus = relay.relays.get(idx);
-        boolean timer = rs.hasTimer != null || (rstatus != null && rstatus.hasTimer != null); // Dimmer 1/2 have
-                                                                                              // has_timer under /status
-        addChannel(thing, add, rs.ison != null, group, CHANNEL_OUTPUT);
-        addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
-        addChannel(thing, add, rs.autoOn != null, group, CHANNEL_TIMER_AUTOON);
-        addChannel(thing, add, rs.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
-        addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
+        if (profile.settings.relays != null) {
+            ShellySettingsRelay rs = profile.settings.relays.get(idx);
+            ShellyShortStatusRelay rstatus = relay.relays.get(idx);
+            boolean timer = rs.hasTimer != null || (rstatus != null && rstatus.hasTimer != null); // Dimmer 1/2 have
+                                                                                                  // has_timer under
+                                                                                                  // /status
+            addChannel(thing, add, rs.ison != null, group, CHANNEL_OUTPUT);
+            addChannel(thing, add, rs.name != null, group, CHANNEL_OUTPUT_NAME);
+            addChannel(thing, add, rs.autoOn != null, group, CHANNEL_TIMER_AUTOON);
+            addChannel(thing, add, rs.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
+            addChannel(thing, add, timer, group, CHANNEL_TIMER_ACTIVE);
+        }
 
         // Shelly 1/1PM Addon
         if (relay.extTemperature != null) {
@@ -329,12 +332,14 @@ public class ShellyChannelDefinitions {
         // Shelly Dimmer has an additional brightness channel
         addChannel(thing, add, profile.isDimmer, group, CHANNEL_BRIGHTNESS);
 
-        ShellySettingsDimmer ds = profile.settings.dimmers.get(idx);
-        addChannel(thing, add, ds.autoOn != null, group, CHANNEL_TIMER_AUTOON);
-        addChannel(thing, add, ds.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
+        if (profile.settings.dimmers != null) {
+            ShellySettingsDimmer ds = profile.settings.dimmers.get(idx);
+            addChannel(thing, add, ds.autoOn != null, group, CHANNEL_TIMER_AUTOON);
+            addChannel(thing, add, ds.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
 
-        ShellyShortLightStatus dss = dstatus.dimmers.get(idx);
-        addChannel(thing, add, dss != null && dss.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+            ShellyShortLightStatus dss = dstatus.dimmers.get(idx);
+            addChannel(thing, add, dss != null && dss.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+        }
         return add;
     }
 
@@ -343,13 +348,16 @@ public class ShellyChannelDefinitions {
         Map<String, Channel> add = new LinkedHashMap<>();
         String group = profile.getControlGroup(idx);
 
-        ShellySettingsRgbwLight light = profile.settings.lights.get(idx);
-        // The is no brightness channel in color mode, so we need a power channel
-        addChannel(thing, add, profile.inColor, group, CHANNEL_LIGHT_POWER);
-
-        addChannel(thing, add, light.autoOn != null, group, CHANNEL_TIMER_AUTOON);
-        addChannel(thing, add, light.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
-        addChannel(thing, add, status.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+        if (profile.settings.lights != null) {
+            ShellySettingsRgbwLight light = profile.settings.lights.get(idx);
+            // Create power channel in color mode and brightness channel in white mode
+            addChannel(thing, add, profile.inColor, group, CHANNEL_LIGHT_POWER);
+            addChannel(thing, add, !profile.inColor, group, CHANNEL_BRIGHTNESS);
+            addChannel(thing, add, light.temp != null, CHANNEL_GROUP_WHITE_CONTROL, CHANNEL_COLOR_TEMP);
+            addChannel(thing, add, light.autoOn != null, group, CHANNEL_TIMER_AUTOON);
+            addChannel(thing, add, light.autoOff != null, group, CHANNEL_TIMER_AUTOOFF);
+            addChannel(thing, add, status.hasTimer != null, group, CHANNEL_TIMER_ACTIVE);
+        }
         return add;
     }
 
@@ -379,7 +387,7 @@ public class ShellyChannelDefinitions {
         return add;
     }
 
-    public static Map<String, Channel> createRollerChannels(Thing thing, final ShellyControlRoller roller) {
+    public static Map<String, Channel> createRollerChannels(Thing thing, final ShellyRollerStatus roller) {
         Map<String, Channel> add = new LinkedHashMap<>();
         addChannel(thing, add, true, CHGR_ROLLER, CHANNEL_ROL_CONTROL_CONTROL);
         addChannel(thing, add, true, CHGR_ROLLER, CHANNEL_ROL_CONTROL_STATE);
index dd7b1ea9296686e1b64aa6550a4bbbf50c8fbb7e..9fd0884c0a9c5183229853fb519732d8bf09cd22 100644 (file)
@@ -19,6 +19,8 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.LocalDateTime;
@@ -351,4 +353,26 @@ public class ShellyUtils {
     public static char lastChar(String s) {
         return s.length() > 1 ? s.charAt(s.length() - 1) : '*';
     }
+
+    public static String sha256(String string) throws ShellyApiException {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            final byte[] hashbytes = digest.digest(string.getBytes(StandardCharsets.UTF_8));
+            return bytesToHex(hashbytes);
+        } catch (NoSuchAlgorithmException e) {
+            throw new ShellyApiException("SHA256 can't be initialzed", e);
+        }
+    }
+
+    public static String bytesToHex(byte[] bytes) {
+        StringBuilder hexString = new StringBuilder(2 * bytes.length);
+        for (int i = 0; i < bytes.length; i++) {
+            String hex = Integer.toHexString(0xff & bytes[i]);
+            if (hex.length() == 1) {
+                hexString.append('0');
+            }
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
 }
index 0ddaebe5ced1539026ed3c3bd703b96b8313d226..9553e75e8d75aea1cbaab594cf3c1e0ee81e410f 100644 (file)
@@ -17,6 +17,7 @@
                        <default>admin</default>
                        <label>@text/binding.shelly.config.defaultPassword.label</label>
                        <description>@text/binding.shelly.config.defaultPassword.description</description>
+                       <context>password</context>
                </parameter>
                <parameter name="localIP" type="text">
                        <label>@text/binding.shelly.config.localIP.label</label>
index 9e1d48a37f286dd9ec070ab687525120cfa1be19..2b4bf674a6b66e4be193aa4fb31c00ecdc566c4d 100644 (file)
@@ -86,6 +86,23 @@ thing-type.shelly.shellygas.description = Shelly Gas (Gas Sensor - Gas Leak Dete
 thing-type.shelly.shellytrv.description = Shelly TRV (Radiator value, battery powered)
 thing-type.shelly.shellyix3.description = Shelly ix3 (Activation Device with 3 inputs)
 
+# Plus/Pro devices
+ thing-type.shelly.shellyplus1.description = Shelly Plus 1 (Single Relay Switch)
+ thing-type.shelly.shellyplus1pm.description =  Shelly Plus 1PM - Single Relay Switch with Power Meter
+ thing-type.shelly.shellyplus2-relay.description = Shelly Plus 2PM - Dual Relay Switch with Power Meter
+ thing-type.shelly.shellyplus2pm-roller.description = Shelly Plus 2PM - Roller Control with Power Meter
+ thing-type.shelly.shellyplusht.description = Shelly Plus HT - Humidity and Temperature sensor with display
+ thing-type.shelly.shellyplusi4.description = Shelly Plus i4 - 4xInput Device
+ thing-type.shelly.shellyplusi4dc.description = Shelly Plus i4DC - 4xDC Input Device
+ thing-type.shelly.shellypro1.description = Shelly Pro 1 - Single Relay Switch
+ thing-type.shelly.shellypro1pm.description = Shelly Pro 1PM - Single Relay Switch with Power Meter
+ thing-type.shelly.shellypro2-relay.description = Shelly Pro 2 - Dual Relay Switch
+ thing-type.shelly.shellypro2-roller.description = Shelly Pro 2 - Roller Control
+ thing-type.shelly.shellypro2pm-relay.description= Shelly Pro 2PM - Dual Relay Switch with Power Meter
+ thing-type.shelly.shellypro2pm-roller.description = Shelly Pro 2PM - Roller Control with Power Meter
+ thing-type.shelly.shellypro3.description = Shelly Pro 3 - 3xRelay Switch
+ thing-type.shelly.shellypro4pm.description = Shelly Pro 4PM - 4xRelay Switch with Power Meter
 # thing config - shellydevice
 thing-type.config.shelly.deviceIp.label = IP Address
 thing-type.config.shelly.deviceIp.description = IP Address of the Shelly device
@@ -247,7 +264,7 @@ channel-type.shelly.meterAccuReturned.description = Accumulated Returned Power i
 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
-channel-type.shelly.lastPower1.description = Rounded power consumption during last minute
+channel-type.shelly.lastPower1.description = Last power consumption #1 - one rounded minute
 channel-type.shelly.meterTotal.label = Total Energy Consumption
 channel-type.shelly.meterTotal.description = Total energy consumption in kW/h since the device powered up (resets on restart)
 channel-type.shelly.meterReturned.label = Total Returned Energy