]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tapocontrol] new communication protocol integration / code revision (#15725)
authorChristian Wild <40909464+wildcs@users.noreply.github.com>
Sun, 28 Apr 2024 07:38:06 +0000 (09:38 +0200)
committerGitHub <noreply@github.com>
Sun, 28 Apr 2024 07:38:06 +0000 (09:38 +0200)
* [tapocontrol] new tapo klap-protocol integration

Signed-off-by: Christian Wild <christian@wildclan.de>
127 files changed:
bundles/org.openhab.binding.tapocontrol/README.md
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoControlHandlerFactory.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoCloudConnector.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoConnectorInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceConnector.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceHttpApi.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolEnum.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthrough.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthroughSession.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePasstroughCipher.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapCipher.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapSession.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/passthrough/PassthroughProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoComConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorCode.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoThingConstants.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartBulb.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginResult.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoBaseDeviceData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildDeviceData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildList.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoEnergyData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightDynamicFx.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightEffect.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/TapoChildDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/smartcontact/TapoSmartContactHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/wheatersensor/TapoWheaterSensorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoBaseDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoDeviceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoUniversalDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbLastStates.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbModeEnum.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketData.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketStripHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoChildDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUdpDiscovery.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResult.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResultList.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoBaseRequestInterface.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoChildRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoMultipleRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/PayloadBuilder.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCredentials.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoEncoder.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoErrorHandler.java
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoKeyPair.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoUtils.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/ByteUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/JsonUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/StringUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TapoUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TypeUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChild.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChildData.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoEnergyData.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoSubRequest.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/bridgeconfig.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml [deleted file]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/deviceconfig.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/hubconfig.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/i18n/tapocontrol.properties
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/H100.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L530.xml
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L900.xml
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L920.xml
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L930.xml
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T110.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T310.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T315.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channelgroups.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channels.xml
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/aurora.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/bubbling_calderon.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/candy_cane.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas_light.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/dynamic_light_fx.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/flicker.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/hanukkah.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/haunted_mansion.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/icicle.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/lightning.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/ocean.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/rainbow.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/raindrop.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/spring.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunrise.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunset.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/valentines.json [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoMDNS.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUDP.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/config/config.xml [deleted file]
bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/thing/testdevice.xml

index b429f3c4691c605d0c14e28ef826784eca720f19..0a458dbb722c419cfabe552cca13cc171ebbe9fa 100644 (file)
@@ -6,20 +6,24 @@ This binding adds support to control Tapo (Copyright Â© TP-Link Corporation Limi
 
 The following Tapo-Devices are supported. For precise channel-description look at `channels-table` below
 
-| DeviceType                         | ThingType   | Description                                 |
-|------------------------------------|-------------|---------------------------------------------|
-| SmartPlug (Wi-Fi)                  | P100        | Smart Socket                                |
-|                                    | P105        | Smart Mini Socket                           |
-| EnergyMonitoring SmartPlug (Wi-Fi) | P110        | Energy Monitoring Smart Socket              |
-|                                    | P115        | Energy Monitoring Mini Smart Socket         |
-| Power Strip (Wi-Fi)                | P300        | Smart Wi-Fi Power Strip - 3 sockets         |
-| Dimmable SmartBulb (Wi-Fi)         | L510        | Dimmable White-Light Smart-Bulb (E27)       |
-|                                    | L610        | Dimmable White-Light Smart-Spot (GU10)      |
-| MultiColor SmartBulb (Wi-Fi)       | L530        | Multicolor Smart-Bulb (E27)                 |
-|                                    | L630        | Multicolor Smart-Spot (GU10)                |
-| MultiColor LightStrip (Wi-Fi)      | L900        | Multicolor RGB Dimmable LightStrip (5m)     |
-|                                    | L920        | Multicolor RGB-IC ColorZone LightStrip (5m) |
-|                                    | L930        | Multicolor RGBW-IC 50-Zone LightStrip (5m)  |
+| DeviceType                         | ThingType   | Description                                  |
+|------------------------------------|-------------|----------------------------------------------|
+| SmartPlug (Wi-Fi)                  | P100        | Smart Socket                                 |
+|                                    | P105        | Smart Mini Socket                            |
+| EnergyMonitoring SmartPlug (Wi-Fi) | P110        | Energy Monitoring Smart Socket               |
+|                                    | P115        | Energy Monitoring Mini Smart Socket          |
+| Power Strip (Wi-Fi)                | P300        | Smart Wi-Fi Power Strip - 3 sockets          |
+| Dimmable SmartBulb (Wi-Fi)         | L510        | Dimmable White-Light Smart-Bulb (E27)        |
+|                                    | L610        | Dimmable White-Light Smart-Spot (GU10)       |
+| MultiColor SmartBulb (Wi-Fi)       | L530        | Multicolor Smart-Bulb (E27)                  |
+|                                    | L630        | Multicolor Smart-Spot (GU10)                 |
+| MultiColor LightStrip (Wi-Fi)      | L900        | Multicolor RGB Dimmable LightStrip (5m)      |
+|                                    | L920        | Multicolor RGB-IC ColorZone LightStrip (5m)  |
+|                                    | L930        | Multicolor RGBW-IC 50-Zone LightStrip (5m)   |
+| Smart Hub (Wi-Fi / RF)             | H100        | Smart Hub with Chime to control Child Devices|
+| Smart Contact Sensor (RF)          | T110        | Window/Door Smart Contact Sensor             |
+| Smart Temperature Sensor (RF)      | T310        | Temperature and Humidity Sensor              |
+|                                    | T315        | Temperature and Humidity Sensor with Display |
 
 ## Prerequisites
 
@@ -32,9 +36,22 @@ To satisfy this requirement while keeping the device isolated, your router shoul
 
 ## Discovery
 
-Discovery is done by connecting to the Tapo-Cloud Service.
-All devices stored in your cloud account will be detected even if they are not in your network.
-You need to know the IP-Adress of your device. This must be set manually in the thing configuration
+For Wi-Fi-Devices, discovery is done by connecting to the Tapo-Cloud-Service or use local udp-discovery.
+If enabled, all devices stored in your cloud account will be detected even if they are not in your local network.
+From cloud you can get more informations such as "Device-Alias" as from udp-discovery.
+But you need to know the IP-Adress of your device. This must be set manually in the thing configuration.
+
+UDP-Discovery can find only devices which are online in your local network and get less informations as from cloud.
+But therefore it set's device-ip and protocol automaticly.
+If you have problems with udp-discovery, try to set the advanced setting 'broadcastAddress' to your local subnet ('e.g. 192.168.0.255').
+Default is '255.255.255.255'
+
+You can combine both discovery methods to get any informations from local devices.  
+If you enable setting 'onlyLocalOnlineDevices' results will only generated for local online devices but with the combined data of cloud discovery.
+RF-Devices will be discovered by the hub they are connected to.
+You can discover them manually or use Â´backgroundDiscovery´ 
+
+RF-Devices will be discovered by the hub they are connected to. You can discover them manually or use Â´backgroundDiscovery´ 
 
 ## Bridge Configuration
 
@@ -43,21 +60,32 @@ This is used for device discovery and to create a handshake (cookie) to act with
 
 The thing has the following configuration parameters:
 
-| Parameter          | Description                                                          |
-|--------------------|----------------------------------------------------------------------|
-| username           | Username (eMail) of your Tapo-Cloud                                  |
-| password           | Password of your Tapo-Cloud                                          |
+| Parameter              | Description                                                                                                    |
+|------------------------|----------------------------------------------------------------------------------------------------------------|
+| username               | Username (eMail) of your Tapo-Cloud                                                                            |
+| password               | Password of your Tapo-Cloud                                                                                    |
+| cloudDiscovery         | Use Cloud Discovery-Service to get all in Tapo-App registered devices. Includes DeviceName. IP-Address and Encryption has to set manually             |
+| udpDiscovery           | Use UDP Discovery-Service to discover online devices in the local network. Includes Encryption and IP-Address. Results will be merged with cloud discovery      |
+| onlyLocalOnlineDevices | [advanced] Uses Cloud and UPD-Discovery to get more informations but will only discover online devices via UDP |
+| broadcastAddress       | [advanced] Set broadcast address to your local subnet  if you have problems with default address               |
+| discoveryInterval      | [advanced] Interval in minutes when a background device scan should be executed. Default is 60                 |
+
 
 ## Thing Configuration
 
-The thing needs to be configured with `ipAddress`.
+WiFi-based things needs to be configured with `ipAddress`.
+RF-based things need a SmartHub(WiFi-Device) to operate.
 
-The thing has the following configuration parameters:
+The things has the following configuration parameters:
+
+| Parameter          | Description                                                           | Things supporting parameter |
+|--------------------|-----------------------------------------------------------------------|-----------------------------|
+| ipAddress          | IP Address of the device.                                             | Any Wi-Fi-Device            |
+| pollingInterval    | [optional] Refresh interval in seconds. The default is 30 seconds     | Any Wi-Fi-Device            |
+| httpPort           | [optional] HTTP-Communication Port. Default is 80                     | Any Wi-Fi-Device            |
+| protocol           | [optional] Used Communication Protocol (AES/KLAP/'') Default 'AES'    | Any Wi-Fi-Device            |
+| backgroundDiscovery| [optional] RF-Devices will be discovered after every polling request  | SmartHub                    |
 
-| Parameter          | Description                                                          |
-|--------------------|----------------------------------------------------------------------|
-| ipAddress          | IP Address of the device.                                            |
-| pollingInterval    | Refresh interval in seconds. Optional. The default is 30 seconds     |
 
 ## Channels
 
@@ -72,12 +100,20 @@ All devices support some of the following channels:
 |           | brightness       | Dimmer                 | Brightness 0-100%                   | L510, L530, L610, L630, L900, L920                               |
 |           | colorTemperature | Number                 | White-Color-Temp 2500-6500K         | L510, L530, L610, L630, L900, L920                               |
 |           | color            | Color                  | Color                               | L530, L630, L900, L920                                           |
-| effects   | fxName           | String                 | Active lightning effect             | L530                                                             |
+| sensor    | isOpen           | Switch                 | Contact (Door/Window) is Open       | T110                                                             |
+|           | currentTemp      | Number:Temperature     | Current Temperature                 | T310, T315                                                       |
+|           | currentHumidity  | Number:Dimensionless   | Current relative humidity in %      | T310, T315                                                       |
+| effects   | fxName           | String                 | Active lightning effect             | L530, L900, L920, L930                                           |
 | device    | wifiSignal       | Number                 | WiFi-quality-level                  | P100, P105, P110, P115, L510, L530, L610, L630, L900, L920, L930 |
 |           | onTime           | Number:Time            | seconds output is on                | P100, P105, P110, P115, L510, L530, L900, L920, L930             |
+|           | signalStrength   | Number                 | RF-quality-level                    | T110                                                             |
+|           | isOnline         | Switch                 | Device is Online                    | T110                                                             |
+|           | batteryLow       | Switch                 | Battery of device is low            | T110                                                             |
 | energy    | actualPower      | Number:Power           | actual Power (Watt)                 | P110, P115                                                       |
 |           | todayEnergyUsage | Number:Energy          | used energy today (Wh)              | P110, P115                                                       |
 |           | todayRuntime     | Number:Time            | seconds output was on today         | P110, P115                                                       |
+| alarm     | alarmActive      | Switch                 | Alarm is currntly active            | H100                                                             |
+|           | alarmSource      | String                 | Source causes active alarm          | H100                                                             |
 
 ## Channel Refresh
 
@@ -90,10 +126,10 @@ To minimize network traffic the default refresh-rate is set to 30 seconds. This
 
 ```java
 tapocontrol:bridge:myTapoBridge                 "Cloud-Login"               [ username="you@yourpovider.com", password="verysecret" ]
-tapocontrol:P100:myTapoBridge:mySocket          "My-Socket"     (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.150", pollingInterval=30 ]
-tapocontrol:L510:myTapoBridge:whiteBulb         "white-light"   (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.151", pollingInterval=30 ]
-tapocontrol:L530:myTapoBridge:colorBulb         "color-light"   (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.152", pollingInterval=30 ]
-tapocontrol:L900:myTapoBridge:myLightStrip      "light-strip"   (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.153", pollingInterval=30 ]
+tapocontrol:P100:myTapoBridge:mySocket          "My-Socket"     (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.150" ]
+tapocontrol:L510:myTapoBridge:whiteBulb         "white-light"   (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.151", httpPort=80, pollingInterval=30, protocol="AES" ]
+tapocontrol:L530:myTapoBridge:colorBulb         "color-light"   (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.152", pollingInterval=30, protocol="KLAP" ]
+tapocontrol:L900:myTapoBridge:myLightStrip      "light-strip"   (tapocontrol:bridge:myTapoBridge)   [ ipAddress="192.168.178.153", pollingInterval=30, protocol="" ]
 
 Bridge tapocontrol:bridge:secondBridgeExample            "Cloud-Login"        [ username="youtoo@anyprovider.com", password="verysecret" ] {
    Thing P110 mySocket   "My-Socket"          [ ipAddress="192.168.101.51", pollingInterval=30 ]
index 855bcc9ecb29af89bea7b7efeb94e26434831c00..0238678c349226a55e0fe4e5a8ed77160f054b41 100644 (file)
@@ -22,11 +22,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.device.TapoLightStrip;
-import org.openhab.binding.tapocontrol.internal.device.TapoSmartBulb;
-import org.openhab.binding.tapocontrol.internal.device.TapoSmartPlug;
-import org.openhab.binding.tapocontrol.internal.device.TapoUniversalDevice;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.rf.smartcontact.TapoSmartContactHandler;
+import org.openhab.binding.tapocontrol.internal.devices.rf.wheatersensor.TapoWheaterSensorHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoUniversalDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.bulb.TapoBulbHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.hub.TapoHubHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.lightstrip.TapoLightStripHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.socket.TapoSocketHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.socket.TapoSocketStripHandler;
 import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.Thing;
@@ -42,8 +46,11 @@ import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
 /**
- * The {@link ThingHandler} is responsible for handling commands, which are
+ * The {@link TapoControlHandlerFactory} is responsible for handling commands, which are
  * sent to one of the channels.
  *
  * @author Christian Wild - Initial contribution
@@ -51,6 +58,8 @@ import org.slf4j.LoggerFactory;
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.tapocontrol")
 @NonNullByDefault
 public class TapoControlHandlerFactory extends BaseThingHandlerFactory {
+    public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation()
+            .create();
     private final Logger logger = LoggerFactory.getLogger(TapoControlHandlerFactory.class);
     private final Set<TapoBridgeHandler> accountHandlers = new HashSet<>();
     private final HttpClient httpClient;
@@ -102,16 +111,24 @@ public class TapoControlHandlerFactory extends BaseThingHandlerFactory {
             TapoBridgeHandler bridgeHandler = new TapoBridgeHandler((Bridge) thing, httpClient);
             accountHandlers.add(bridgeHandler);
             return bridgeHandler;
-        } else if (SUPPORTED_SMART_PLUG_UIDS.contains(thingTypeUID)) {
-            return new TapoSmartPlug(thing);
+        } else if (SUPPORTED_HUB_UIDS.contains(thingTypeUID)) {
+            return new TapoHubHandler(thing);
+        } else if (SUPPORTED_SOCKET_UIDS.contains(thingTypeUID)) {
+            return new TapoSocketHandler(thing);
+        } else if (SUPPORTED_SOCKET_STRIP_UIDS.contains(thingTypeUID)) {
+            return new TapoSocketStripHandler(thing);
         } else if (SUPPORTED_WHITE_BULB_UIDS.contains(thingTypeUID)) {
-            return new TapoSmartBulb(thing);
+            return new TapoBulbHandler(thing);
         } else if (SUPPORTED_COLOR_BULB_UIDS.contains(thingTypeUID)) {
-            return new TapoSmartBulb(thing);
+            return new TapoBulbHandler(thing);
         } else if (SUPPORTED_LIGHT_STRIP_UIDS.contains(thingTypeUID)) {
-            return new TapoLightStrip(thing);
+            return new TapoLightStripHandler(thing);
+        } else if (SUPPORTED_SMART_CONTACTS.contains(thingTypeUID)) {
+            return new TapoSmartContactHandler(thing);
+        } else if (SUPPORTED_WHEATHER_SENSORS.contains(thingTypeUID)) {
+            return new TapoWheaterSensorHandler(thing);
         } else if (thingTypeUID.equals(UNIVERSAL_THING_TYPE)) {
-            return new TapoUniversalDevice(thing);
+            return new TapoUniversalDeviceHandler(thing);
         }
         return null;
     }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java
deleted file mode 100644 (file)
index 8fe5df7..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
-import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
-import org.openhab.core.config.discovery.DiscoveryResult;
-import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.config.discovery.DiscoveryService;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.ServiceScope;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home thing discovery
- *
- * @author Christian Wild - Initial contribution
- */
-@Component(scope = ServiceScope.PROTOTYPE, service = TapoDiscoveryService.class)
-@NonNullByDefault
-public class TapoDiscoveryService extends AbstractThingHandlerDiscoveryService<TapoBridgeHandler> {
-    private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
-
-    /***********************************
-     *
-     * INITIALIZATION
-     *
-     ************************************/
-
-    /**
-     * INIT CLASS
-     */
-    public TapoDiscoveryService() {
-        super(TapoBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
-    }
-
-    @Override
-    public void initialize() {
-        thingHandler.setDiscoveryService(this);
-        TapoBridgeConfiguration config = thingHandler.getBridgeConfig();
-        modified(Map.of(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, config.cloudDiscovery));
-        super.initialize();
-    }
-
-    /***********************************
-     *
-     * SCAN HANDLING
-     *
-     ************************************/
-
-    /**
-     * Start scan manually
-     */
-    @Override
-    public void startScan() {
-        removeOlderResults(getTimestampOfLastScan());
-        JsonArray jsonArray = thingHandler.getDeviceList();
-        handleCloudDevices(jsonArray);
-    }
-
-    /***********************************
-     *
-     * handle Results
-     *
-     ************************************/
-
-    /**
-     * CREATE DISCOVERY RESULT
-     * creates discoveryResult (Thing) from JsonObject got from Cloud
-     * 
-     * @param device JsonObject with device information
-     * @return DiscoveryResult-Object
-     */
-    public DiscoveryResult createResult(JsonObject device) {
-        String deviceModel = getDeviceModel(device);
-        String label = getDeviceLabel(device);
-        String deviceMAC = device.get(CLOUD_JSON_KEY_MAC).getAsString();
-        ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
-        /* create properties */
-        Map<String, Object> properties = new HashMap<>();
-        properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
-        properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(deviceMAC, MAC_DIVISION_CHAR));
-        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.get(CLOUD_JSON_KEY_FW).getAsString());
-        properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.get(CLOUD_JSON_KEY_HW).getAsString());
-        properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
-        properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.get(CLOUD_JSON_KEY_ID).getAsString());
-
-        logger.debug("device {} discovered", deviceModel);
-        ThingUID bridgeUID = thingHandler.getUID();
-        ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
-        return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
-                .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
-                .build();
-    }
-
-    /**
-     * work with result from get devices from cloud devices
-     * 
-     * @param deviceList
-     */
-    protected void handleCloudDevices(JsonArray deviceList) {
-        try {
-            for (JsonElement deviceElement : deviceList) {
-                if (deviceElement.isJsonObject()) {
-                    JsonObject device = deviceElement.getAsJsonObject();
-                    String deviceModel = getDeviceModel(device);
-                    ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
-                    /* create thing */
-                    if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
-                        DiscoveryResult discoveryResult = createResult(device);
-                        thingDiscovered(discoveryResult);
-                    }
-                }
-            }
-        } catch (Exception e) {
-            logger.debug("error handling CloudDevices", e);
-        }
-    }
-
-    /**
-     * GET DEVICEMODEL
-     * 
-     * @param device JsonObject with deviceData
-     * @return String with DeviceModel
-     */
-    protected String getDeviceModel(JsonObject device) {
-        try {
-            String deviceModel = device.get(CLOUD_JSON_KEY_MODEL).getAsString();
-            deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
-            deviceModel = deviceModel.replace("Tapo", "");
-            deviceModel = deviceModel.replace("Series", "");
-            deviceModel = deviceModel.trim();
-            deviceModel = deviceModel.replace(" ", "_");
-            return deviceModel;
-        } catch (Exception e) {
-            logger.debug("error getDeviceModel", e);
-            return "";
-        }
-    }
-
-    /**
-     * GET DEVICE LABEL
-     * 
-     * @param device JsonObject with deviceData
-     * @return String with DeviceLabel
-     */
-    protected String getDeviceLabel(JsonObject device) {
-        try {
-            String deviceLabel = "";
-            String deviceModel = getDeviceModel(device);
-            ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
-            if (SUPPORTED_SMART_PLUG_UIDS.contains(deviceUID)) {
-                deviceLabel = DEVICE_DESCRIPTION_SMART_PLUG;
-            } else if (SUPPORTED_WHITE_BULB_UIDS.contains(deviceUID)) {
-                deviceLabel = DEVICE_DESCRIPTION_WHITE_BULB;
-            } else if (SUPPORTED_COLOR_BULB_UIDS.contains(deviceUID)) {
-                deviceLabel = DEVICE_DESCRIPTION_COLOR_BULB;
-            }
-            return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
-        } catch (Exception e) {
-            logger.debug("error getDeviceLabel", e);
-            return "";
-        }
-    }
-}
index 0fdafd9ca7d3050e93f382c77dd1effba694b90c..1dcbb2ee215f65ba68ab11704c95e732cc6d0039 100644 (file)
 package org.openhab.binding.tapocontrol.internal.api;
 
 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
 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.HttpMethod;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
+import org.openhab.binding.tapocontrol.internal.api.protocol.passthrough.PassthroughProtocol;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.dto.TapoCloudLoginData;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.dto.TapoCloudLoginResult;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResultList;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.gson.Gson;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-
 /**
  * Handler class for TAPO-Cloud connections.
  *
  * @author Christian Wild - Initial contribution
  */
 @NonNullByDefault
-public class TapoCloudConnector {
+public class TapoCloudConnector implements TapoConnectorInterface {
     private final Logger logger = LoggerFactory.getLogger(TapoCloudConnector.class);
     private final TapoBridgeHandler bridge;
-    private final Gson gson = new Gson();
-    private final HttpClient httpClient;
 
+    private @NonNullByDefault({}) TapoCloudLoginResult loginResult;
     private String token = "";
-    private String url = TAPO_CLOUD_URL;
     private String uid;
+    private PassthroughProtocol passthrough;
 
-    /**
-     * INIT CLASS
-     * 
-     */
-    public TapoCloudConnector(TapoBridgeHandler bridge, HttpClient httpClient) {
+    /***********************
+     * Init Class
+     **********************/
+
+    public TapoCloudConnector(TapoBridgeHandler bridge) {
         this.bridge = bridge;
-        this.httpClient = httpClient;
         this.uid = bridge.getUID().getAsString();
+        passthrough = new PassthroughProtocol(this);
     }
 
+    /***********************
+     * Response-Handling
+     **********************/
+
     /**
-     * handle error
-     * 
-     * @param tapoError TapoErrorHandler
+     * handle received reponse-string
      */
-    protected void handleError(TapoErrorHandler tapoError) {
-        this.bridge.setError(tapoError);
+    @Override
+    public void responsePasstrough(String response, String command) {
     }
 
-    /***********************************
-     *
-     * HTTP (Cloud)-Actions
-     *
-     ************************************/
-
     /**
-     * LOGIN TO CLOUD (get Token)
-     * 
-     * @param username unencrypted username
-     * @param password unencrypted password
-     * @return true if login was successfull
+     * handle received response
      */
-    public Boolean login(String username, String password) {
-        this.token = getToken(username, password, UUID.randomUUID().toString());
-        this.url = TAPO_CLOUD_URL + "?token=" + token;
-        return !this.token.isBlank();
+    @Override
+    public void handleResponse(TapoResponse tapoResponse, String command) throws TapoErrorHandler {
+        switch (command) {
+            case CLOUD_CMD_LOGIN:
+                handleLoginResult(tapoResponse);
+                break;
+            case CLOUD_CMD_GETDEVICES:
+                bridge.getDiscoveryService().addScanResults(getDeviceListFromResponse(tapoResponse));
+                break;
+            default:
+                logger.debug("({}) handleResponse - unknown command: {}", uid, command);
+                throw new TapoErrorHandler(ERR_BINDING_NOT_IMPLEMENTED);
+        }
     }
 
     /**
-     * logout
+     * set bridge error
      */
-    public void logout() {
-        this.token = "";
+    @Override
+    public void handleError(TapoErrorHandler tapoError) {
+        bridge.setError(tapoError);
     }
 
+    /***********************
+     * Login Handling
+     **********************/
     /**
-     * GET TOKEN FROM TAPO-CLOUD
-     * 
-     * @param email
-     * @param password
-     * @param terminalUUID
-     * @return
+     * login to cloud
      */
-    private String getToken(String email, String password, String terminalUUID) {
-        String token = "";
-
-        /* create login payload */
-        PayloadBuilder plBuilder = new PayloadBuilder();
-        plBuilder.method = "login";
-        plBuilder.addParameter("appType", TAPO_APP_TYPE);
-        plBuilder.addParameter("cloudUserName", email);
-        plBuilder.addParameter("cloudPassword", password);
-        plBuilder.addParameter("terminalUUID", terminalUUID);
-        String payload = plBuilder.getPayload();
-
-        ContentResponse response = sendCloudRequest(TAPO_CLOUD_URL, payload);
-        if (response != null) {
-            token = getTokenFromResponse(response);
-        }
-        return token;
+    public boolean login(TapoCredentials credentials) throws TapoErrorHandler {
+        logout();
+
+        TapoCloudLoginData loginData = new TapoCloudLoginData(credentials.username(), credentials.password());
+        TapoRequest request = new TapoRequest(CLOUD_CMD_LOGIN, loginData);
+
+        passthrough.sendRequest(request);
+        return isLoggedIn();
     }
 
-    private String getTokenFromResponse(ContentResponse response) {
-        /* work with response */
-        if (response.getStatus() == 200) {
-            String rBody = response.getContentAsString();
-            JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class);
-            if (jsonObject != null) {
-                Integer errorCode = jsonObject.get("error_code").getAsInt();
-                if (errorCode == 0) {
-                    token = jsonObject.getAsJsonObject("result").get("token").getAsString();
-                } else {
-                    /* return errorcode from device */
-                    String msg = jsonObject.get("msg").getAsString();
-                    handleError(new TapoErrorHandler(errorCode, msg));
-                    logger.trace("cloud returns error: '{}'", rBody);
-                }
-            } else {
-                handleError(new TapoErrorHandler(ERR_API_JSON_DECODE_FAIL));
-                logger.trace("unexpected json-response '{}'", rBody);
-            }
-        } else {
-            handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
-            logger.warn("invalid response while login");
-            token = "";
+    /*
+     * get response from login and set token
+     */
+    private void handleLoginResult(TapoResponse tapoResponse) throws TapoErrorHandler {
+        logger.trace("({}) received login result: {}", uid, tapoResponse);
+        loginResult = getObjectFromJson(tapoResponse.result(), TapoCloudLoginResult.class);
+        token = loginResult.token();
+        if (!isLoggedIn()) {
+            throw new TapoErrorHandler(ERR_API_LOGIN);
         }
-        return token;
     }
 
+    public void logout() {
+        loginResult = new TapoCloudLoginResult();
+        token = "";
+    }
+
+    public boolean isLoggedIn() {
+        return !token.isBlank();
+    }
+
+    /***********************
+     * Cloud Action Handlers
+     **********************/
     /**
-     * 
-     * @return JsonArray with deviceList
+     * Query DeviceList from cloud
      */
-    public JsonArray getDeviceList() {
-        /* create payload */
-        PayloadBuilder plBuilder = new PayloadBuilder();
-        plBuilder.method = "getDeviceList";
-        String payload = plBuilder.getPayload();
-
-        ContentResponse response = sendCloudRequest(this.url, payload);
-        if (response != null) {
-            return getDeviceListFromResponse(response);
+    public void getDeviceList() {
+        TapoRequest request = new TapoRequest(CLOUD_CMD_GETDEVICES);
+        logger.trace("({}) sending cloud command: {}", uid, request);
+        try {
+            passthrough.sendRequest(request);
+        } catch (TapoErrorHandler tapoError) {
+            logger.debug("({}) get devicelist failed: {}", uid, tapoError.getCode());
+            handleError(tapoError);
         }
-        return new JsonArray();
     }
 
     /**
-     * get DeviceList from Contenresponse
-     * 
-     * @param response
-     * @return
+     * get DeviceList from response
      */
-    private JsonArray getDeviceListFromResponse(ContentResponse response) {
-        /* work with response */
-        if (response.getStatus() == 200) {
-            String rBody = response.getContentAsString();
-            JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class);
-            if (jsonObject != null) {
-                /* get errocode (0=success) */
-                Integer errorCode = jsonObject.get("error_code").getAsInt();
-                if (errorCode == 0) {
-                    JsonObject result = jsonObject.getAsJsonObject("result");
-                    return result.getAsJsonArray("deviceList");
-                } else {
-                    /* return errorcode from device */
-                    handleError(new TapoErrorHandler(errorCode, "device answers with errorcode"));
-                    logger.trace("cloud returns error: '{}'", rBody);
-                }
-            } else {
-                logger.trace("enexpected json-response '{}'", rBody);
-            }
-        } else {
-            logger.trace("response error '{}'", response.getContentAsString());
-        }
-        return new JsonArray();
+    private TapoDiscoveryResultList getDeviceListFromResponse(TapoResponse tapoResponse) throws TapoErrorHandler {
+        logger.trace("({}) received devicelist: {}", uid, tapoResponse);
+        return getObjectFromJson(tapoResponse.result(), TapoDiscoveryResultList.class);
     }
 
-    /***********************************
-     *
-     * HTTP-ACTIONS
-     *
-     ************************************/
+    /***********************
+     * Get Values
+     **********************/
+
+    @Override
+    public HttpClient getHttpClient() {
+        return bridge.getHttpClient();
+    }
+
+    @Override
+    public String getThingUID() {
+        return bridge.getUID().toString();
+    }
+
+    /************************
+     * Private Helpers
+     ************************/
+
     /**
-     * SEND SYNCHRON HTTP-REQUEST
-     * 
-     * @param url url request is sent to
-     * @param payload payload (String) to send
-     * @return ContentResponse of request
+     * Get Cloud-URL
      */
-    @Nullable
-    protected ContentResponse sendCloudRequest(String url, String payload) {
-        Request httpRequest = httpClient.newRequest(url).method(HttpMethod.POST.toString());
-        httpRequest.timeout(TAPO_HTTP_CLOUD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    @Override
+    public String getBaseUrl() {
+        String url = TAPO_CLOUD_URL;
+        if (!token.isBlank()) {
+            url = url + "?token=" + token;
+        }
+        return url;
+    }
 
-        /* set header */
+    /**
+     * Set http-headers
+     */
+    public Request setHeaders(Request httpRequest) {
         httpRequest.header("content-type", CONTENT_TYPE_JSON);
         httpRequest.header("Accept", CONTENT_TYPE_JSON);
-
-        /* add request body */
-        httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
-
-        try {
-            return httpRequest.send();
-        } catch (InterruptedException e) {
-            logger.debug("({}) sending request interrupted: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(e));
-        } catch (TimeoutException e) {
-            logger.debug("({}) sending request timeout: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, e.toString()));
-        } catch (Exception e) {
-            logger.debug("({}) sending request failed: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(e));
-        }
-        return null;
+        return httpRequest;
     }
 }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoConnectorInterface.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoConnectorInterface.java
new file mode 100644 (file)
index 0000000..e2a8197
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * Interface for TAPO-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public interface TapoConnectorInterface {
+
+    /* get http-client from bridge */
+    HttpClient getHttpClient();
+
+    /* handle received taporesponse */
+    public void handleResponse(TapoResponse tapoResponse, String command) throws TapoErrorHandler;
+
+    /* handle error */
+    public void handleError(TapoErrorHandler e);
+
+    /* handle received reponse-string */
+    public void responsePasstrough(String response, String command);
+
+    /* get base url of device */
+    public String getBaseUrl();
+
+    /* geth ThingUID of device */
+    public String getThingUID();
+}
index c8f88d02784c184bf8e3c46b76f87c2b2691f4b8..d5d2024a0b49d7cdfd487649ebdf8eff89a4ba11 100644 (file)
  */
 package org.openhab.binding.tapocontrol.internal.api;
 
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.jsonObjectToInt;
 
 import java.net.InetAddress;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
-import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolEnum;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
+import org.openhab.binding.tapocontrol.internal.api.protocol.aes.SecurePassthrough;
+import org.openhab.binding.tapocontrol.internal.api.protocol.klap.KlapProtocol;
+import org.openhab.binding.tapocontrol.internal.api.protocol.passthrough.PassthroughProtocol;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoChildRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoMultipleRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoChild;
-import org.openhab.binding.tapocontrol.internal.structures.TapoChildData;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoEnergyData;
-import org.openhab.binding.tapocontrol.internal.structures.TapoSubRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
 
 /**
- * Handler class for TAPO Smart Home device connections.
+ * Connection handler class for TAPO wifi devices.
  * This class uses asynchronous HttpClient-Requests
  *
  * @author Christian Wild - Initial contribution
  */
 @NonNullByDefault
-public class TapoDeviceConnector extends TapoDeviceHttpApi {
-
+public class TapoDeviceConnector implements TapoConnectorInterface {
     private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
-
-    private TapoDeviceInfo deviceInfo = new TapoDeviceInfo();
-    private TapoEnergyData energyData = new TapoEnergyData();
-    private TapoChildData childData = new TapoChildData();
+    private final TapoBaseDeviceHandler device;
+    private final TapoBridgeHandler bridge;
+    private final String uid;
+    private TapoProtocolInterface protocolHandler;
+    private TapoResponse queryResponse = new TapoResponse();
     private long lastQuery = 0L;
     private long lastSent = 0L;
     private long lastLogin = 0L;
+    private boolean queryAfterCommand = false;
+
+    /***********************
+     * Init Class
+     **********************/
+
+    public TapoDeviceConnector(TapoBaseDeviceHandler tapoDeviceHandler, TapoBridgeHandler bridgeThingHandler) {
+        bridge = bridgeThingHandler;
+        device = tapoDeviceHandler;
+        uid = device.getThingUID().toString() + " / DeviceConnector";
+        protocolHandler = setProtocol(device.getDeviceConfig().protocol);
+    }
 
     /**
-     * INIT CLASS
-     *
-     * @param device
-     * @param bridgeThingHandler
+     * Set DeviceProtocol which is used for communication
      */
-    public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
-        super(device, bridgeThingHandler);
+    protected TapoProtocolInterface setProtocol(String protocol) {
+        switch (TapoProtocolEnum.valueOfString(protocol)) {
+            case PASSTHROUGH:
+                logger.trace("({}) selected passtrough-protocol '{}'", uid, protocol);
+                return new PassthroughProtocol(this);
+            case SECUREPASSTROUGH:
+                logger.trace("({}) selected secure-passtrough-protocol '{}'", uid, protocol);
+                return new SecurePassthrough(this);
+            case KLAP:
+                logger.trace("({}) selected klap-protocol '{}'", uid, protocol);
+                return new KlapProtocol(this);
+            default:
+                logger.debug("({}) unknown protocol '{}'", uid, protocol);
+                handleError(new TapoErrorHandler(NO_ERROR, protocol));
+                return new PassthroughProtocol(this);
+        }
     }
 
-    /***********************************
-     *
+    /***********************
      * LOGIN FUNCTIONS
-     *
-     ************************************/
-    /**
-     * login
-     *
-     * @return true if success
-     */
-    public boolean login() {
-        if (this.pingDevice()) {
-            logger.trace("({}) sending login to url '{}'", uid, deviceURL);
+     **********************/
 
+    public boolean login() {
+        if (pingDevice()) {
             long now = System.currentTimeMillis();
-            if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
-                this.lastLogin = now;
-                unsetToken();
-                unsetCookie();
-
-                /* create ssl-handschake (cookie) */
-                String cookie = createHandshake();
-                if (!cookie.isBlank()) {
-                    setCookie(cookie);
-                    String token = queryToken();
-                    setToken(token);
+            if (now > lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
+                lastLogin = now;
+                try {
+                    protocolHandler.login(bridge.getCredentials());
+                } catch (TapoErrorHandler tapoError) {
+                    logger.debug("({}) exception while login '{}'", uid, tapoError.toString());
+                    handleError(tapoError);
                 }
-            } else {
-                logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
             }
-            return this.loggedIn();
         } else {
-            logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
+            logger.debug("({}) no ping while login '{}'", uid, device.getIpAddress());
             handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE, "no ping while login"));
-            return false;
         }
+        return protocolHandler.isLoggedIn();
     }
 
-    /***********************************
-     *
+    public void logout() {
+        protocolHandler.logout();
+    }
+
+    public boolean isLoggedIn() {
+        return protocolHandler.isLoggedIn();
+    }
+
+    /***********************
      * DEVICE ACTIONS
-     *
-     ************************************/
+     **********************/
 
     /**
-     * send custom command to device
+     * Send raw (unsecured) request to device
+     */
+    public void sendRawCommand(TapoRequest request) {
+        try {
+            PassthroughProtocol passtrhough = new PassthroughProtocol(this);
+            passtrhough.sendAsyncRequest(request);
+        } catch (TapoErrorHandler tapoError) {
+            logger.debug("({}) send raw command failed '{}'", uid, tapoError.toString());
+            handleError(tapoError);
+        }
+    }
+
+    /**
+     * Send DeviceQueryCommand to Device
      *
-     * @param queryMethod query method
+     * @param queryCommand Command to be queried
      */
-    public void sendCustomQuery(String queryMethod) {
-        /* create payload */
-        PayloadBuilder plBuilder = new PayloadBuilder();
-        plBuilder.method = queryMethod;
-        sendCustomPayload(plBuilder);
+    public void sendQueryCommand(String queryCommand) {
+        sendQueryCommand(queryCommand, false);
     }
 
     /**
-     * send custom command to device
+     * Send Custom DeviceQuery
      *
-     * @param plBuilder Payloadbuilder with unencrypted payload
+     * @param queryCommand Command to be queried
+     * @param ignoreGap ignore gap to last query. query anyway
      */
-    public void sendCustomPayload(PayloadBuilder plBuilder) {
+    public void sendQueryCommand(String queryCommand, boolean ignoreGap) {
+        queryAfterCommand = false;
         long now = System.currentTimeMillis();
-        if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
-            String payload = plBuilder.getPayload();
-            sendSecurePasstrhroug(payload, DEVICE_CMD_CUSTOM);
+        if (ignoreGap || now > lastQuery + TAPO_QUERY_MIN_GAP_MS) {
+            lastQuery = now;
+            sendAsyncRequest(new TapoRequest(queryCommand));
         } else {
-            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+            logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastQuery);
         }
     }
 
     /**
-     * send "set_device_info" command to device
-     *
-     * @param name Name of command to send
-     * @param value Value to send to control
+     * Send "set_device_info" command to device and query info immediately
+     * 
+     * @param deviceDataClass clazz contains devicedata which should be sent
      */
-    public void sendDeviceCommand(String name, Object value) {
-        sendDeviceCommand(DEVICE_CMD_SETINFO, name, value);
+    public void sendDeviceCommand(Object deviceDataClass) {
+        sendDeviceCommand(DEVICE_CMD_SETINFO, deviceDataClass);
     }
 
     /**
-     * send "set_device_info" command to device
-     *
-     * @param method Method command belongs to
-     * @param name Name of command to send
-     * @param value Value to send to control
+     * Send command to device with params and query info immediately
+     * 
+     * @param command command
+     * @param deviceDataClass clazz contains devicedata which should be sent
      */
-    public void sendDeviceCommand(String method, String name, Object value) {
-        long now = System.currentTimeMillis();
-        if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
-            this.lastSent = now;
-
-            /* create payload */
-            PayloadBuilder plBuilder = new PayloadBuilder();
-            plBuilder.method = method;
-            plBuilder.addParameter(name, value);
-            String payload = plBuilder.getPayload();
-
-            sendSecurePasstrhroug(payload, method);
-        } else {
-            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
-        }
+    public void sendDeviceCommand(String command, Object deviceDataClass) {
+        sendDeviceCommand(command, deviceDataClass, false);
     }
 
     /**
-     * send "set_device_info" command to child's device
-     *
-     * @param index of the child
-     * @param childProperty to modify
-     * @param value for the property
+     * Send command to device with params
+     * 
+     * @param command command
+     * @param deviceDataClass clazz contains devicedata which should be sent
+     * @param ignoreGap ignore gap to last query. query anyway
      */
-    public void sendChildCommand(Integer index, String childProperty, Object value) {
+    public void sendDeviceCommand(String command, Object deviceDataClass, boolean ignoreGap) {
+        queryAfterCommand = false;
         long now = System.currentTimeMillis();
-        if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
-            this.lastSent = now;
-            getChild(index).ifPresent(child -> {
-                child.setDeviceOn(Boolean.valueOf((Boolean) value));
-                TapoSubRequest request = new TapoSubRequest(child.getDeviceId(), DEVICE_CMD_SETINFO, child);
-                sendSecurePasstrhroug(GSON.toJson(request), request.method());
-            });
+        if (ignoreGap || now > lastSent + TAPO_SEND_MIN_GAP_MS) {
+            sendAsyncRequest(new TapoRequest(command, deviceDataClass));
         } else {
-            logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
+            logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastSent);
         }
     }
 
     /**
-     * send multiple "set_device_info" commands to device
-     *
-     * @param map {@code HashMap<String, Object> (name, value of parameter)}
+     * Send command to device with params and query info immediately
+     * 
+     * @param deviceDataClass clazz contains devicedata which should be sent
+     * @param multipleRequestSupported set to true if device supports multipleRequests
      */
-    public void sendDeviceCommands(HashMap<String, Object> map) {
-        sendDeviceCommands(DEVICE_CMD_SETINFO, map);
+    public void sendCommandAndQuery(Object deviceDataClass, boolean multipleRequestSupported) {
+        sendCommandAndQuery(DEVICE_CMD_SETINFO, deviceDataClass, multipleRequestSupported);
     }
 
     /**
-     * send multiple commands to device
-     *
-     * @param method Method command belongs to
-     * @param map {@code HashMap<String, Object> (name, value of parameter)}
+     * Send command to device with params and query info immediately
+     * 
+     * @param command command
+     * @param deviceDataClass clazz contains devicedata which should be sent
+     * @param multipleRequestSupported set to true if device supports multipleRequests
      */
-    public void sendDeviceCommands(String method, HashMap<String, Object> map) {
-        long now = System.currentTimeMillis();
-        if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
-            this.lastSent = now;
-
-            /* create payload */
-            PayloadBuilder plBuilder = new PayloadBuilder();
-            plBuilder.method = method;
-            for (HashMap.Entry<String, Object> entry : map.entrySet()) {
-                plBuilder.addParameter(entry.getKey(), entry.getValue());
-            }
-            String payload = plBuilder.getPayload();
-
-            sendSecurePasstrhroug(payload, method);
+    public void sendCommandAndQuery(String command, Object deviceDataClass, boolean multipleRequestSupported) {
+        if (multipleRequestSupported) {
+            List<TapoRequest> requests = new ArrayList<>();
+            requests.add(new TapoRequest(command, deviceDataClass));
+            requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+            sendAsyncRequest(new TapoMultipleRequest(requests));
         } else {
-            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+            sendDeviceCommand(command, deviceDataClass, true);
+            queryAfterCommand = true;
         }
     }
 
     /**
-     * Query Info from Device and refresh deviceInfo
+     * Send command to child device
+     * 
+     * @param childData ChildDeviceData-Class
      */
-    public void queryInfo() {
-        queryInfo(false);
+    public void sendChildCommand(TapoChildDeviceData childData) {
+        sendChildCommand(childData, false);
     }
 
     /**
-     * Query Info from Device and refresh deviceInfo
-     *
-     *
+     * Send command to child device
+     * 
+     * @param childData ChildDeviceData-Class
      * @param ignoreGap ignore gap to last query. query anyway
      */
-    public void queryInfo(boolean ignoreGap) {
-        logger.trace("({}) DeviceConnector_queryInfo from '{}'", uid, deviceURL);
-        queryCommand(DEVICE_CMD_GETINFO, ignoreGap);
+    public void sendChildCommand(TapoChildDeviceData childData, boolean ignoreGap) {
+        long now = System.currentTimeMillis();
+        if (ignoreGap || now > lastSent + TAPO_SEND_MIN_GAP_MS) {
+            lastSent = now;
+            sendAsyncRequest(new TapoChildRequest(childData));
+        } else {
+            logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastSent);
+        }
     }
 
     /**
-     * Query Info from Child Devices and refresh deviceInfo
+     * send asynchronous multi-request
+     *
+     * @param requests list of TapoRequests should be sent to device
      */
-    @Override
-    public void queryChildDevices() {
-        logger.trace("({}) DeviceConnector_queryChildDevices from '{}'", uid, deviceURL);
-        queryCommand(DEVICE_CMD_CHILD_DEVICE_LIST, true);
+    public void sendMultipleRequest(List<TapoRequest> requests) {
+        sendMultipleRequest(requests, false);
     }
 
     /**
-     * Get energy usage from device
+     * send asynchronous multi-request igrnoring min-gap
+     * 
+     * @param requests list of TapoRequests should be sent to device
+     * @param ignoreGap ignoreGap ignore gap to last query. query anyway
      */
-    public void getEnergyUsage() {
-        queryCommand(DEVICE_CMD_GETENERGY, true);
+    public void sendMultipleRequest(List<TapoRequest> requests, boolean ignoreGap) {
+        queryAfterCommand = false;
+        long now = System.currentTimeMillis();
+        if (ignoreGap || now > lastQuery + TAPO_QUERY_MIN_GAP_MS) {
+            lastQuery = now;
+            sendAsyncRequest(new TapoMultipleRequest(requests));
+        } else {
+            logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastSent);
+        }
     }
 
     /**
-     * Send Custom DeviceQuery
-     *
-     * @param queryCommand Command to be queried
-     * @param ignoreGap ignore gap to last query. query anyway
+     * send asynchron multi-request
+     * 
+     * @param requests array of TapoRequest
      */
-    public void queryCommand(String queryCommand, boolean ignoreGap) {
-        logger.trace("({}) DeviceConnector_queryCommand '{}' from '{}'", uid, queryCommand, deviceURL);
-        long now = System.currentTimeMillis();
-        if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
-            this.lastQuery = now;
-
-            /* create payload */
-            PayloadBuilder plBuilder = new PayloadBuilder();
-            plBuilder.method = queryCommand;
-            String payload = plBuilder.getPayload();
-
-            sendSecurePasstrhroug(payload, queryCommand);
-        } else {
-            logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastQuery);
-        }
+    public void sendMultipleRequest(TapoRequest... requests) {
+        sendMultipleRequest(requests);
     }
 
     /**
-     * SEND SECUREPASSTHROUGH
-     * encprypt payload and send to device
-     *
-     * @param payload payload sent to device
-     * @param command command executed - this will handle result
+     * send asynchron request to protocol handler
+     * 
+     * @param tapoRequest Request inherits TapoBaseRequestInterface
      */
-    protected void sendSecurePasstrhroug(String payload, String command) {
-        /* encrypt payload */
-        logger.trace("({}) encrypting payload '{}'", uid, payload);
-        String encryptedPayload = encryptPayload(payload);
+    public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) {
+        try {
+            protocolHandler.sendAsyncRequest(tapoRequest);
+        } catch (TapoErrorHandler tapoError) {
+            handleError(tapoError);
+        }
+    }
 
-        /* create secured payload */
-        PayloadBuilder plBuilder = new PayloadBuilder();
-        plBuilder.method = "securePassthrough";
-        plBuilder.addParameter("request", encryptedPayload);
-        String securePassthroughPayload = plBuilder.getPayload();
+    /***********************
+     * Response-Handling
+     **********************/
 
-        sendAsyncRequest(deviceURL, securePassthroughPayload, command);
+    /**
+     * Return class object from json formated string
+     * 
+     * @param json json formatted string
+     * @param clazz class string should parsed to
+     */
+    private <T> T getObjectFromJson(String json, Class<T> clazz) {
+        try {
+            @Nullable
+            T result = GSON.fromJson(json, clazz);
+            if (result == null) {
+                throw new JsonParseException("result is null");
+            }
+            return result;
+        } catch (Exception e) {
+            logger.debug("({}) error parsing string {} to class: {}", uid, json, clazz.getName());
+            device.setError(new TapoErrorHandler(ERR_API_JSON_DECODE_FAIL));
+            return Objects.requireNonNull(GSON.fromJson(json, clazz));
+        }
     }
 
-    /***********************************
-     *
-     * HANDLE RESPONSES
-     *
-     ************************************/
+    /**
+     * Return class object from JsonObject
+     * 
+     * @param jso JsonOject
+     * @param clazz class string should parsed to
+     */
+    private <T> T getObjectFromJson(JsonObject jso, Class<T> clazz) {
+        return getObjectFromJson(jso.toString(), clazz);
+    }
 
     /**
-     * Handle SuccessResponse (setDeviceInfo)
-     *
-     * @param responseBody String with responseBody from device
+     * handle and decrypt response from device
+     * 
+     * @param response TapoResponse was received
+     * @param command was sent to device belonging to response
      */
     @Override
-    protected void handleSuccessResponse(String responseBody) {
-        JsonObject jsnResult = getJsonFromResponse(responseBody);
-        Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
-        if (errorCode != 0) {
-            logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
-            this.device.handleConnectionState();
+    public void handleResponse(TapoResponse response, String command) {
+        if (!response.hasError()) {
+            if (DEVICE_CMD_MULTIPLE_REQ.equals(command)) {
+                handleMultipleRespone(response);
+            } else {
+                handleSingleResponse(response, command);
+            }
+        } else {
+            device.setError(new TapoErrorHandler(response.errorCode()));
         }
-        this.device.responsePasstrough(responseBody);
     }
 
     /**
-     *
-     * handle JsonResponse (getDeviceInfo)
-     *
-     * @param responseBody String with responseBody from device
+     * Handle response got from single-request
+     * 
+     * @param response response from request
+     * @param command command was sent
      */
-    @Override
-    protected void handleDeviceResult(String responseBody) {
-        JsonObject jsnResult = getJsonFromResponse(responseBody);
-        if (jsnResult.has(JSON_KEY_ID)) {
-            this.deviceInfo = new TapoDeviceInfo(jsnResult);
-            this.device.setDeviceInfo(deviceInfo);
+    private void handleSingleResponse(TapoResponse response, String command) {
+        logger.trace("({}) handle singleResponse from command '{}'", uid, command);
+        if (DEVICE_CMDLIST_QUERY.contains(command)) {
+            handleQueryResult(response, command);
+        } else if (DEVICE_CMDLIST_SET.contains(command)) {
+            handleSuccessResponse(response);
         } else {
-            this.deviceInfo = new TapoDeviceInfo();
-            this.device.handleConnectionState();
+            device.responsePasstrough(response);
         }
-        this.device.responsePasstrough(responseBody);
     }
 
     /**
-     * handle JsonResponse (getEnergyData)
-     *
-     * @param responseBody String with responseBody from device
+     * Handle response got from multiple-request
+     * 
+     * @param response response from request
      */
-    @Override
-    protected void handleEnergyResult(String responseBody) {
-        JsonObject jsnResult = getJsonFromResponse(responseBody);
-        if (jsnResult.has(JSON_KEY_ENERGY_POWER)) {
-            this.energyData = new TapoEnergyData(jsnResult);
-            this.device.setEnergyData(energyData);
-        } else {
-            this.energyData = new TapoEnergyData();
+    private void handleMultipleRespone(TapoResponse response) {
+        logger.trace("({}) handle multipleResponse '{}'", uid, response);
+        for (TapoResponse results : response.responses()) {
+            handleSingleResponse(results, results.method());
         }
-        this.device.responsePasstrough(responseBody);
     }
 
     /**
-     * handle JsonResponse (getChildDeviceList)
-     *
-     * @param responseBody String with responseBody from device
+     * Parse responsedata from result to object and inform device about new data
+     * 
+     * @param response response from request
+     * @param command command was sent
      */
-    @Override
-    protected void handleChildDevices(String responseBody) {
-        JsonObject jsnResult = getJsonFromResponse(responseBody);
-        if (jsnResult.has(JSON_KEY_CHILD_START_INDEX)) {
-            this.childData = Objects.requireNonNull(GSON.fromJson(jsnResult, TapoChildData.class));
-            this.device.setChildData(childData);
+    private void handleQueryResult(TapoResponse response, String command) {
+        if (!response.hasError()) {
+            logger.trace("({}) queryResponse successfull '{}'", uid, response);
+            queryResponse = response;
+            device.newDataResult(command);
         } else {
-            this.childData = new TapoChildData();
+            logger.debug("({}) query response returned error: {}", uid, response);
+            device.setError(new TapoErrorHandler(queryResponse.errorCode()));
         }
-        this.device.responsePasstrough(responseBody);
     }
 
     /**
-     * handle custom response
-     *
-     * @param responseBody String with responseBody from device
+     * Handle SuccessResponse (setDeviceInfo)
+     * 
+     * @param response response from request
      */
-    @Override
-    protected void handleCustomResponse(String responseBody) {
-        this.device.responsePasstrough(responseBody);
+    private void handleSuccessResponse(TapoResponse response) {
+        if (response.hasError()) {
+            logger.debug("({}) set deviceInfo not successful: {}", uid, response);
+            device.setError(new TapoErrorHandler(response.errorCode()));
+        } else {
+            logger.trace("({}) setcommand successfull '{}'", uid, response);
+            if (queryAfterCommand) {
+                sendQueryCommand(DEVICE_CMD_GETINFO, true);
+            }
+        }
+        queryAfterCommand = false;
+        this.device.responsePasstrough(response);
     }
 
     /**
-     * handle error
+     * Handle custom response
      *
-     * @param tapoError TapoErrorHandler
+     * @param response response from request
      */
-    @Override
-    protected void handleError(TapoErrorHandler tapoError) {
-        this.device.setError(tapoError);
+    protected void handleCustomResponse(TapoResponse response) {
+        logger.trace("({}) handle custom response '{}'", uid, response);
+        this.device.responsePasstrough(response);
     }
 
     /**
-     * get Json from response
-     *
-     * @param responseBody
-     * @return JsonObject with result
+     * handle error
      */
-    private JsonObject getJsonFromResponse(String responseBody) {
-        JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
-        /* get errocode (0=success) */
-        if (jsonObject != null) {
-            Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
-            if (errorCode == 0) {
-                /* decrypt response */
-                jsonObject = GSON.fromJson(responseBody, JsonObject.class);
-                logger.trace("({}) received result: {}", uid, responseBody);
-                if (jsonObject != null) {
-                    /* return result if set / else request was successful */
-                    if (jsonObject.has("result")) {
-                        return jsonObject.getAsJsonObject("result");
-                    } else {
-                        return jsonObject;
-                    }
-                }
-            } else {
-                /* return errorcode from device */
-                TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
-                logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
-                handleError(te);
-                return jsonObject;
-            }
-        }
-        logger.debug("({}) sendPayload exception {}", uid, responseBody);
-        handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
-        return new JsonObject();
+    @Override
+    public void handleError(TapoErrorHandler tapoError) {
+        logger.debug("({}) handle error '{}'", uid, tapoError.getMessage());
+        device.setError(tapoError);
     }
 
-    /***********************************
-     *
-     * GET RESULTS
-     *
-     ************************************/
+    /***********************
+     * Get Values
+     **********************/
 
     /**
      * Check if device is online
      *
      * @return true if device is online
      */
-    public Boolean isOnline() {
+    public boolean isOnline() {
         return isOnline(false);
     }
 
     /**
      * Check if device is online
      *
-     * @param raiseError if true
-     * @return true if device is online
+     * @param raiseError if true - if false it will be only logout();
      */
-    public Boolean isOnline(Boolean raiseError) {
+    public boolean isOnline(boolean raiseError) {
         if (pingDevice()) {
             return true;
         } else {
@@ -463,28 +472,26 @@ public class TapoDeviceConnector extends TapoDeviceHttpApi {
             if (raiseError) {
                 handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE));
             }
-            logout();
+            protocolHandler.logout();
             return false;
         }
     }
 
     /**
-     * IP-Adress
-     *
-     * @return String ipAdress
+     * Get Dataobject from response
+     * 
+     * @param clazz object class response should be transformed
      */
-    public String getIP() {
-        return this.ipAddress;
+    public <T> T getResponseData(Class<T> clazz) {
+        return getObjectFromJson(queryResponse.result(), clazz);
     }
 
     /**
-     * PING IP Adress
-     *
-     * @return true if ping successfull
+     * Ping to IP of device - return true if successfull
      */
-    public Boolean pingDevice() {
+    public boolean pingDevice() {
         try {
-            InetAddress address = InetAddress.getByName(this.ipAddress);
+            InetAddress address = InetAddress.getByName(device.getIpAddress());
             return address.isReachable(TAPO_PING_TIMEOUT_MS);
         } catch (Exception e) {
             logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
@@ -492,7 +499,23 @@ public class TapoDeviceConnector extends TapoDeviceHttpApi {
         }
     }
 
-    private Optional<TapoChild> getChild(int position) {
-        return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
+    @Override
+    public HttpClient getHttpClient() {
+        return bridge.getHttpClient();
+    }
+
+    @Override
+    public void responsePasstrough(String response, String command) {
+        throw new UnsupportedOperationException("Unimplemented method 'responsePasstrough'");
+    }
+
+    @Override
+    public String getBaseUrl() {
+        return device.getIpAddress() + ":" + device.getDeviceConfig().httpPort;
+    }
+
+    @Override
+    public String getThingUID() {
+        return device.getThingUID().toString();
     }
 }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceHttpApi.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceHttpApi.java
deleted file mode 100644 (file)
index 4382b0d..0000000
+++ /dev/null
@@ -1,622 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.api;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpResponse;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.client.util.BufferingResponseListener;
-import org.eclipse.jetty.client.util.StringContentProvider;
-import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
-import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoCipher;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home device connections.
- * This class uses synchronous HttpClient-Requests for login to device
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoDeviceHttpApi {
-    protected static final Gson GSON = new GsonBuilder()
-            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
-
-    private final Logger logger = LoggerFactory.getLogger(TapoDeviceHttpApi.class);
-    private final TapoCipher tapoCipher;
-    private final TapoBridgeHandler bridge;
-    protected final String uid;
-    protected final TapoDevice device;
-
-    private String token = "";
-    private String cookie = "";
-    protected String deviceURL = "";
-    protected String ipAddress = "";
-
-    /**
-     * INIT CLASS
-     *
-     * @param device
-     * @param bridgeThingHandler
-     */
-    public TapoDeviceHttpApi(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
-        this.bridge = bridgeThingHandler;
-        this.tapoCipher = new TapoCipher();
-        this.device = device;
-        this.uid = device.getThingUID().getAsString();
-        setDeviceURL(device.getIpAddress());
-    }
-
-    /***********************************
-     *
-     * DELEGATING FUNCTIONS
-     * will normaly be delegated to extension-classes(TapoDeviceConnector)
-     *
-     ************************************/
-    /**
-     * handle SuccessResponse (setDeviceInfo)
-     *
-     * @param responseBody String with responseBody from device
-     */
-    protected void handleSuccessResponse(String responseBody) {
-    }
-
-    /**
-     * handle JsonResponse (getDeviceInfo)
-     *
-     * @param responseBody String with responseBody from device
-     */
-    protected void handleDeviceResult(String responseBody) {
-    }
-
-    /**
-     * handle JsonResponse (getEnergyData)
-     *
-     * @param responseBody String with responseBody from device
-     */
-    protected void handleEnergyResult(String responseBody) {
-    }
-
-    /**
-     * handle custom response
-     *
-     * @param responseBody String with responseBody from device
-     */
-    protected void handleCustomResponse(String responseBody) {
-    }
-
-    /**
-     * handle JsonResponse (getChildDevices)
-     *
-     * @param responseBody String with responseBody from device
-     */
-    protected void handleChildDevices(String responseBody) {
-    }
-
-    /**
-     * handle error
-     *
-     * @param tapoError TapoErrorHandler
-     */
-    protected void handleError(TapoErrorHandler tapoError) {
-    }
-
-    /**
-     * refresh the list of child devices
-     *
-     */
-    protected void queryChildDevices() {
-    }
-
-    /***********************************
-     *
-     * LOGIN FUNCTIONS
-     *
-     ************************************/
-
-    /**
-     * Create Handshake and set cookie
-     *
-     * @return true if handshake (cookie) was created
-     */
-    protected String createHandshake() {
-        String cookie = "";
-        try {
-            /* create payload for handshake */
-            PayloadBuilder plBuilder = new PayloadBuilder();
-            plBuilder.method = "handshake";
-            plBuilder.addParameter("key", bridge.getCredentials().getPublicKey()); // ?.decode("UTF-8")
-            String payload = plBuilder.getPayload();
-
-            /* send request (create ) */
-            logger.trace("({}) create handhsake with payload: {}", uid, payload);
-            ContentResponse response = sendRequest(this.deviceURL, payload);
-            if (response != null && getErrorCode(response) == 0) {
-                String encryptedKey = getKeyFromResponse(response);
-                this.tapoCipher.setKey(encryptedKey, bridge.getCredentials());
-                cookie = getCookieFromResponse(response);
-            }
-        } catch (Exception e) {
-            logger.debug("({}) could not createHandshake: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, "could not createHandshake"));
-        }
-        return cookie;
-    }
-
-    /**
-     * return encrypted key from 'handshake' request
-     *
-     * @param response ContentResponse from "handshake" method
-     * @return
-     */
-    private String getKeyFromResponse(ContentResponse response) {
-        String rBody = response.getContentAsString();
-        JsonObject jsonObj = GSON.fromJson(rBody, JsonObject.class);
-        if (jsonObj != null) {
-            logger.trace("({}) received awnser: {}", uid, rBody);
-            return jsonObjectToString(jsonObj.getAsJsonObject("result"), "key");
-        } else {
-            logger.warn("({}) could not getKeyFromResponse '{}'", uid, rBody);
-            handleError(new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, "could not getKeyFromResponse"));
-        }
-        return "";
-    }
-
-    /**
-     * return cookie from 'handshake' request
-     *
-     * @param response ContentResponse from "handshake" metho
-     * @return
-     */
-    private String getCookieFromResponse(ContentResponse response) {
-        String cookie = "";
-        try {
-            cookie = response.getHeaders().get("Set-Cookie").split(";")[0];
-            logger.trace("({}) got cookie: '{}'", uid, cookie);
-        } catch (Exception e) {
-            logger.warn("({}) could not getCookieFromResponse", uid);
-            handleError(new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, "could not getCookieFromResponse"));
-        }
-        return cookie;
-    }
-
-    /**
-     * Query Token from device
-     *
-     * @return String with token returned from device
-     */
-    protected String queryToken() {
-        String token = "";
-        try {
-            /* encrypt login credentials */
-            PayloadBuilder plBuilder = new PayloadBuilder();
-            plBuilder.method = "login_device";
-            plBuilder.addParameter("username", bridge.getCredentials().getEncodedEmail());
-            plBuilder.addParameter("password", bridge.getCredentials().getEncodedPassword());
-            String payload = plBuilder.getPayload();
-            String encryptedPayload = this.encryptPayload(payload);
-
-            /* create secured login informations */
-            plBuilder = new PayloadBuilder();
-            plBuilder.method = "securePassthrough";
-            plBuilder.addParameter("request", encryptedPayload);
-            String securePassthroughPayload = plBuilder.getPayload();
-
-            /* sendRequest and get Token */
-            ContentResponse response = sendRequest(deviceURL, securePassthroughPayload);
-            token = getTokenFromResponse(response);
-        } catch (Exception e) {
-            logger.debug("({}) error building login payload: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(e, "error building login payload"));
-        }
-        return token;
-    }
-
-    /**
-     * get Token from "login"-request
-     *
-     * @param response
-     * @return
-     */
-    private String getTokenFromResponse(@Nullable ContentResponse response) {
-        String result = "";
-        TapoErrorHandler tapoError = new TapoErrorHandler();
-        if (response != null && response.getStatus() == 200) {
-            String rBody = response.getContentAsString();
-            String decryptedResponse = this.decryptResponse(rBody);
-            logger.trace("({}) received result: {}", uid, decryptedResponse);
-
-            /* get errocode (0=success) */
-            JsonObject jsonObject = GSON.fromJson(decryptedResponse, JsonObject.class);
-            if (jsonObject != null) {
-                Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
-                if (errorCode == 0) {
-                    /* return result if set / else request was successful */
-                    result = jsonObjectToString(jsonObject.getAsJsonObject("result"), "token");
-                } else {
-                    /* return errorcode from device */
-                    tapoError.raiseError(errorCode, "could not get token");
-                    logger.debug("({}) login recieved errorCode {} - {}", uid, errorCode, tapoError.getMessage());
-                }
-            } else {
-                logger.debug("({}) unexpected json-response '{}'", uid, decryptedResponse);
-                tapoError.raiseError(ERR_API_JSON_ENCODE_FAIL, "could not get token");
-            }
-        } else {
-            logger.debug("({}) invalid response while login", uid);
-            tapoError.raiseError(ERR_BINDING_HTTP_RESPONSE, "invalid response while login");
-        }
-        /* handle error */
-        if (tapoError.hasError()) {
-            handleError(tapoError);
-        }
-        return result;
-    }
-
-    /***********************************
-     *
-     * HTTP-ACTIONS
-     *
-     ************************************/
-    /**
-     * SEND SYNCHRON HTTP-REQUEST
-     *
-     * @param url url request is sent to
-     * @param payload payload (String) to send
-     * @return ContentResponse of request
-     */
-    @Nullable
-    protected ContentResponse sendRequest(String url, String payload) {
-        logger.trace("({}) sendRequest to '{}' with cookie '{}'", uid, url, this.cookie);
-
-        Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
-
-        /* set header */
-        httpRequest = setHeaders(httpRequest);
-        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-
-        /* add request body */
-        httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
-
-        try {
-            return httpRequest.send();
-        } catch (InterruptedException e) {
-            logger.debug("({}) sending request interrupted: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(e));
-        } catch (TimeoutException e) {
-            logger.debug("({}) sending request timeout: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, e.toString()));
-        } catch (Exception e) {
-            logger.debug("({}) sending request failed: {}", uid, e.toString());
-            handleError(new TapoErrorHandler(e));
-        }
-        return null;
-    }
-
-    /**
-     * SEND ASYNCHRONOUS HTTP-REQUEST
-     * (don't wait for awnser with programm code)
-     *
-     * @param url string url request is sent to
-     * @param payload data-payload
-     * @param command command executed - this will handle RepsonseType
-     */
-    protected void sendAsyncRequest(String url, String payload, String command) {
-        logger.trace("({}) sendAsncRequest to '{}' with cookie '{}'", uid, url, this.cookie);
-        logger.trace("({}) command/payload: '{}''{}'", uid, command, payload);
-
-        try {
-            Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
-
-            /* set header */
-            httpRequest = setHeaders(httpRequest);
-
-            /* add request body */
-            httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
-
-            httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
-                @NonNullByDefault({})
-                @Override
-                public void onComplete(Result result) {
-                    final HttpResponse response = (HttpResponse) result.getResponse();
-                    if (result.getFailure() != null) {
-                        /* handle result errors */
-                        Throwable e = result.getFailure();
-                        String errorMessage = getValueOrDefault(e.getMessage(), "");
-                        if (e instanceof TimeoutException) {
-                            logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
-                            handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
-                        } else {
-                            logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
-                            handleError(new TapoErrorHandler(new Exception(e), errorMessage));
-                        }
-                    } else if (response.getStatus() != 200) {
-                        logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
-                        handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
-                    } else {
-                        /* request successful */
-                        String rBody = getContentAsString();
-                        logger.trace("({}) receivedRespose '{}'", uid, rBody);
-                        if (!hasErrorCode(rBody)) {
-                            rBody = decryptResponse(rBody);
-                            logger.trace("({}) decryptedResponse '{}'", uid, rBody);
-                            /* handle result */
-                            switch (command) {
-                                case DEVICE_CMD_SETINFO:
-                                    handleSuccessResponse(rBody);
-                                    break;
-                                case DEVICE_CMD_GETINFO:
-                                    handleDeviceResult(rBody);
-                                    break;
-                                case DEVICE_CMD_GETENERGY:
-                                    handleEnergyResult(rBody);
-                                    break;
-                                case DEVICE_CMD_CUSTOM:
-                                    handleCustomResponse(rBody);
-                                    break;
-                                case DEVICE_CMD_CHILD_DEVICE_LIST:
-                                    handleChildDevices(rBody);
-                                    break;
-                            }
-                        } else {
-                            getErrorCode(rBody);
-                        }
-                    }
-                }
-            });
-        } catch (Exception e) {
-            handleError(new TapoErrorHandler(e));
-        }
-    }
-
-    /**
-     * return error code from response
-     *
-     * @param response
-     * @return 0 if request was successfull
-     */
-    protected Integer getErrorCode(@Nullable ContentResponse response) {
-        try {
-            if (response != null) {
-                String responseBody = response.getContentAsString();
-                return getErrorCode(responseBody);
-            } else {
-                return ERR_BINDING_HTTP_RESPONSE.getCode();
-            }
-        } catch (Exception e) {
-            return ERR_BINDING_HTTP_RESPONSE.getCode();
-        }
-    }
-
-    /**
-     * return error code from responseBody
-     *
-     * @param responseBody
-     * @return 0 if request was successfull
-     */
-    protected Integer getErrorCode(String responseBody) {
-        try {
-            JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
-            /* get errocode (0=success) */
-            Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
-            if (errorCode == 0) {
-                return 0;
-            } else {
-                logger.debug("({}) device returns errorcode '{}'", uid, errorCode);
-                handleError(new TapoErrorHandler(errorCode));
-                return errorCode;
-            }
-        } catch (Exception e) {
-            return ERR_BINDING_HTTP_RESPONSE.getCode();
-        }
-    }
-
-    /**
-     * Check for JsonObject "errorcode" and if this is > 0 (no Error)
-     *
-     * @param responseBody
-     * @return true if is js errorcode > 0; false if there is no "errorcode"
-     */
-    protected Boolean hasErrorCode(String responseBody) {
-        if (isValidJson(responseBody)) {
-            JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
-            /* get errocode (0=success) */
-            Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
-            if (errorCode > 0) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * SET HTTP-HEADERS
-     */
-    private Request setHeaders(Request httpRequest) {
-        /* set header */
-        httpRequest.header("content-type", CONTENT_TYPE_JSON);
-        httpRequest.header("Accept", CONTENT_TYPE_JSON);
-        if (!this.cookie.isEmpty()) {
-            httpRequest.header(HTTP_AUTH_TYPE_COOKIE, this.cookie);
-        }
-        return httpRequest;
-    }
-
-    /***********************************
-     *
-     * ENCRYPTION / CODING
-     *
-     ************************************/
-
-    /**
-     * Decrypt Response
-     *
-     * @param responseBody encrypted string from response-body
-     * @return String decrypted responseBody
-     */
-    protected String decryptResponse(String responseBody) {
-        try {
-            JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
-            if (jsonObject != null) {
-                String encryptedResponse = jsonObjectToString(jsonObject.getAsJsonObject("result"), "response");
-                return tapoCipher.decode(encryptedResponse);
-            } else {
-                handleError(new TapoErrorHandler(ERR_API_JSON_DECODE_FAIL));
-            }
-        } catch (Exception ex) {
-            logger.debug("({}) exception '{}' decryptingResponse: '{}'", uid, ex.toString(), responseBody);
-        }
-        return responseBody;
-    }
-
-    /**
-     * encrypt payload
-     *
-     * @param payload
-     * @return encrypted payload
-     */
-    protected String encryptPayload(String payload) {
-        try {
-            return tapoCipher.encode(payload);
-        } catch (Exception ex) {
-            logger.debug("({}) exception encoding Payload '{}'", uid, ex.toString());
-            return "";
-        }
-    }
-
-    /**
-     * perform logout (dispose cookie)
-     */
-    public void logout() {
-        logger.trace("DeviceHttpApi_logout");
-        unsetToken();
-        unsetCookie();
-    }
-
-    /***********************************
-     *
-     * GET RESULTS
-     *
-     ************************************/
-    /**
-     * Logged In
-     *
-     * @return true if logged in
-     */
-    public Boolean loggedIn() {
-        return loggedIn(false);
-    }
-
-    /**
-     * Logged In
-     *
-     * @param raiseError if true
-     * @return true if logged in
-     */
-    public Boolean loggedIn(Boolean raiseError) {
-        if (!this.token.isBlank() && !this.cookie.isBlank()) {
-            return true;
-        } else {
-            logger.trace("({}) not logged in", uid);
-            if (raiseError) {
-                handleError(new TapoErrorHandler(ERR_API_LOGIN));
-            }
-            return false;
-        }
-    }
-
-    /***********************************
-     *
-     * SET VALUES
-     *
-     ************************************/
-
-    /**
-     * Set new ipAddress
-     *
-     * @param ipAddress new ipAdress
-     */
-    public void setDeviceURL(String ipAddress) {
-        this.ipAddress = ipAddress;
-        this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
-    }
-
-    /**
-     * Set new ipAdresss with token
-     *
-     * @param ipAddress ipAddres of device
-     * @param token token from login-ressult
-     */
-    public void setDeviceURL(String ipAddress, String token) {
-        this.ipAddress = ipAddress;
-        this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
-    }
-
-    /**
-     * Set new token
-     *
-     * @param token
-     */
-    protected void setToken(String token) {
-        if (!token.isBlank()) {
-            String url = this.deviceURL.replaceAll("\\?token=\\w*", "");
-            this.deviceURL = url + "?token=" + token;
-        }
-        this.token = token;
-    }
-
-    /**
-     * Unset Token (device logout)
-     */
-    protected void unsetToken() {
-        this.deviceURL = this.deviceURL.replaceAll("\\?token=\\w*", "");
-        this.token = "";
-    }
-
-    /**
-     * Set new cookie
-     *
-     * @param cookie
-     */
-    protected void setCookie(String cookie) {
-        this.cookie = cookie;
-    }
-
-    /**
-     * Unset Cookie (device logout)
-     */
-    protected void unsetCookie() {
-        bridge.getHttpClient().getCookieStore().removeAll();
-        this.cookie = "";
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolEnum.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolEnum.java
new file mode 100644 (file)
index 0000000..1d70744
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol;
+
+import java.util.EnumSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Enumeration for Tapo-Protocols
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public enum TapoProtocolEnum {
+    PASSTHROUGH(""),
+    SECUREPASSTROUGH("AES"),
+    KLAP("KLAP");
+
+    public final String value;
+
+    private TapoProtocolEnum(String value) {
+        this.value = value;
+    }
+
+    public static TapoProtocolEnum valueOfString(String label) {
+        return EnumSet.allOf(TapoProtocolEnum.class).stream().filter(p -> p.value.equals(label)).findFirst()
+                .orElseThrow(() -> new IllegalArgumentException());
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return getValue();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolInterface.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolInterface.java
new file mode 100644 (file)
index 0000000..469c88e
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * Interface for TAPO-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public interface TapoProtocolInterface {
+
+    /***********************
+     * Login Handling
+     **********************/
+    public boolean login(TapoCredentials credentials) throws TapoErrorHandler;
+
+    public void logout();
+
+    public boolean isLoggedIn();
+
+    /***********************
+     * Request Sender
+     **********************/
+    /* send synchron request - response will be handled in [responseReceived()] function */
+    public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler;
+
+    /* send asynchron request - response will be handled in [asyncResponseReceived()] function */
+    public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler;
+
+    /************************
+     * RESPONSE HANDLERS
+     ************************/
+
+    /* handle synchron request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function */
+    public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler;
+
+    /* handle asynchron request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function */
+    public void asyncResponseReceived(String content, String command) throws TapoErrorHandler;
+
+    /************************
+     * PRIVATE HELPERS
+     ************************/
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthrough.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthrough.java
new file mode 100644 (file)
index 0000000..2d3f65b
--- /dev/null
@@ -0,0 +1,276 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.aes;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO-SECUREPASSTHROUGH-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class SecurePassthrough implements TapoProtocolInterface {
+    private final Logger logger = LoggerFactory.getLogger(SecurePassthrough.class);
+    protected final TapoConnectorInterface httpDelegator;
+    private final SecurePassthroughSession session;
+    private final String uid;
+
+    /***********************
+     * Init Class
+     **********************/
+
+    public SecurePassthrough(TapoConnectorInterface httpDelegator) {
+        this.httpDelegator = httpDelegator;
+        session = new SecurePassthroughSession(this);
+        uid = httpDelegator.getThingUID() + " / HTTP-SecurePasstrhough";
+    }
+
+    /***********************
+     * Login Handling
+     **********************/
+
+    @Override
+    public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
+        logger.trace("({}) login to device", uid);
+        session.reset();
+        session.login(tapoCredentials);
+        return session.isHandshakeComplete();
+    }
+
+    @Override
+    public void logout() {
+        logger.trace("({}) logout from device", uid);
+        session.reset();
+    }
+
+    @Override
+    public boolean isLoggedIn() {
+        return session.isHandshakeComplete();
+    }
+
+    /***********************
+     * Request Sender
+     **********************/
+
+    /*
+     * send synchron request - request will be sent encrypted (secured)
+     * response will be handled in [responseReceived()] function
+     */
+    @Override
+    public void sendRequest(TapoRequest request) throws TapoErrorHandler {
+        sendRequest(request, true);
+    }
+
+    /*
+     * send synchron request - response will be handled in [responseReceived()] funktion
+     * 
+     * @param encrypt - if false response will be sent unsecured
+     */
+    private void sendRequest(TapoRequest tapoRequest, boolean encrypt) throws TapoErrorHandler {
+        String url = getUrl();
+        String command = tapoRequest.method();
+        logger.trace("({}) sending unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+        if (encrypt) {
+            tapoRequest = session.encryptRequest(tapoRequest);
+            logger.trace("({}) encrypted request: '{}' with cookie '{}'", uid, tapoRequest, session.getCookie());
+        }
+
+        Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header */
+        httpRequest = setHeaders(httpRequest);
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        /* add request body */
+        httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+        try {
+            responseReceived(httpRequest.send(), command);
+        } catch (Exception e) {
+            throw new TapoErrorHandler(e, "error sending request");
+        }
+    }
+
+    /*
+     * send asynchron request - request will be sent encrypted (secured)
+     * response will be handled in [asyncResponseReceived()] function
+     */
+    @Override
+    public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
+        String url = getUrl();
+        String command = tapoRequest.method();
+        logger.trace("({}) sendAsync unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+
+        TapoRequest encryptedRequest = session.encryptRequest(tapoRequest);
+        logger.trace("({}) sending encrypted request to '{}' with cookie '{}'", uid, url, session.getCookie());
+
+        Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header */
+        httpRequest = setHeaders(httpRequest);
+
+        /* add request body */
+        httpRequest.content(new StringContentProvider(encryptedRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
+            @NonNullByDefault({})
+            @Override
+            public void onComplete(Result result) {
+                final HttpResponse response = (HttpResponse) result.getResponse();
+                if (result.getFailure() != null) {
+                    /* handle result errors */
+                    Throwable e = result.getFailure();
+                    String errorMessage = getValueOrDefault(e.getMessage(), "");
+                    /* throw errors to delegator */
+                    if (e instanceof TimeoutException) {
+                        logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
+                        httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
+                    } else {
+                        logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
+                        httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
+                    }
+                } else if (response.getStatus() != 200) {
+                    logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
+                    httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
+                } else {
+                    /* request successful */
+                    String rBody = getContentAsString();
+                    try {
+                        asyncResponseReceived(rBody, command);
+                    } catch (TapoErrorHandler tapoError) {
+                        httpDelegator.handleError(tapoError);
+                    }
+                }
+            }
+        });
+    }
+
+    /************************
+     * RESPONSE HANDLERS
+     ************************/
+
+    /**
+     * handle synchron request-response
+     * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
+        logger.trace("({}) recived response: {}", uid, response.getContentAsString());
+        TapoResponse tapoResponse = getTapoResponse(response);
+        httpDelegator.handleResponse(tapoResponse, command);
+        httpDelegator.responsePasstrough(response.getContentAsString(), command);
+    }
+
+    /**
+     * handle asynchron request-response
+     * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
+        logger.trace("({}) asyncResponseReceived '{}'", uid, content);
+        try {
+            TapoResponse tapoResponse = getTapoResponse(content);
+            httpDelegator.handleResponse(tapoResponse, command);
+        } catch (TapoErrorHandler tapoError) {
+            httpDelegator.handleError(tapoError);
+        }
+    }
+
+    /**
+     * Get Tapo-Response from Contentresponse
+     * decrypt if is encrypted
+     */
+    protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
+        if (response.getStatus() == 200) {
+            return getTapoResponse(response.getContentAsString());
+        } else {
+            logger.debug("({}) invalid response received", uid);
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /**
+     * Get Tapo-Response from responsestring
+     * decrypt if is encrypted
+     */
+    protected TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
+        if (isValidJson(responseString)) {
+            TapoResponse tapoResponse = Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
+            if (tapoResponse.result().has("response")) {
+                tapoResponse = session.decryptResponse(tapoResponse);
+            }
+            if (tapoResponse.hasError()) {
+                throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+            }
+            return tapoResponse;
+        } else {
+            logger.debug("({}) invalid response received", uid);
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /************************
+     * PRIVATE HELPERS
+     ************************/
+
+    /**
+     * Get Url requests are sent to
+     */
+    protected String getUrl() {
+        String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
+        if (session.isHandshakeComplete()) {
+            return baseUrl + "?token=" + session.getToken();
+        } else {
+            return baseUrl;
+        }
+    }
+
+    /**
+     * Set HTTP-Headers
+     */
+    protected Request setHeaders(Request httpRequest) {
+        httpRequest.header("content-type", CONTENT_TYPE_JSON);
+        httpRequest.header("Accept", CONTENT_TYPE_JSON);
+        if (session.isHandshakeComplete()) {
+            httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());
+        }
+        return httpRequest;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthroughSession.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthroughSession.java
new file mode 100644 (file)
index 0000000..f78fff3
--- /dev/null
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.aes;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+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.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoKeyPair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO-SECUREPASSTHROUGH-SESSION
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class SecurePassthroughSession {
+    private static final int RSA_KEYSIZE = 1024;
+
+    private final Logger logger = LoggerFactory.getLogger(SecurePassthroughSession.class);
+
+    private final SecurePasstroughCipher cipher = new SecurePasstroughCipher();
+    private final TapoKeyPair tapoKeyPair;
+    private final SecurePassthrough spth;
+    private String cookie = "";
+    private String token = "";
+    private String uid;
+
+    // List of class-specific commands
+    public static final String DEVICE_CMD_GET_TOKEN = "login_device";
+    public static final String DEVICE_CMD_GET_COOKIE = "handshake";
+    public static final String DEVICE_CMD_SECURE_METHOD = "securePassthrough";
+
+    public SecurePassthroughSession(SecurePassthrough sptHandler) {
+        tapoKeyPair = new TapoKeyPair(RSA_KEYSIZE);
+        spth = sptHandler;
+        uid = spth.httpDelegator.getThingUID() + " / SecurePassthrough-Session";
+    }
+
+    public void reset() {
+        unsetCookie();
+        unsetToken();
+    }
+
+    /***********************
+     * Request Sender
+     **********************/
+
+    private ContentResponse sendHandshakeRequest(TapoRequest tapoRequest, boolean encrypt)
+            throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+        String url = spth.getUrl();
+        logger.trace("({}) sending Handshake request: '{}' to '{}' ", uid, tapoRequest, url);
+        Request httpRequest = spth.httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        if (encrypt) {
+            tapoRequest = encryptRequest(tapoRequest);
+            logger.trace("({}) encrypted request: '{}' with cookie '{}'", uid, tapoRequest, cookie);
+        }
+
+        /* set header */
+        httpRequest = spth.setHeaders(httpRequest);
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        /* add request body */
+        httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+        return httpRequest.send();
+    }
+
+    /************************
+     * HANDSHAKE & COOKIE
+     ************************/
+    /**
+     * Create Handshake
+     */
+    public boolean login(TapoCredentials credentials) throws TapoErrorHandler {
+        reset();
+        try {
+            TapoCredentials encodedCredentials = encodeCredentials(credentials);
+            createHandshake();
+            createToken(encodedCredentials);
+        } catch (TapoErrorHandler e) {
+            throw e;
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_API_LOGIN_FAILED);
+        }
+        return isHandshakeComplete();
+    }
+
+    /************************
+     * HANDSHAKE & COOKIE
+     ************************/
+
+    /**
+     * Create Handshake and get cookie
+     */
+    private void createHandshake() throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+        /* create handshake request */
+        JsonObject json = new JsonObject();
+        json.addProperty("key", tapoKeyPair.getPublicKey());
+        TapoRequest handshakeRequest = new TapoRequest(DEVICE_CMD_GET_COOKIE, json);
+        /* send request */
+        logger.trace("({}) create handhsake with payload: {}", uid, handshakeRequest);
+        ContentResponse response = sendHandshakeRequest(handshakeRequest, false);
+        handleHandshakeResponse(response);
+    }
+
+    /**
+     * work with response from handshake request
+     * get cookie from request and set cipher
+     */
+    private void handleHandshakeResponse(ContentResponse response) throws TapoErrorHandler {
+        /* setCookie */
+        String result = response.getHeaders().get("Set-Cookie").split(";")[0];
+        setCookie(result);
+
+        TapoResponse tapoResponse = spth.getTapoResponse(response);
+        if (!tapoResponse.hasError()) {
+            String encryptedKey = tapoResponse.result().get("key").getAsString();
+            cipher.setKey(encryptedKey, tapoKeyPair);
+        } else {
+            logger.debug("({}) could not createHandshake: {} ({})", uid, tapoResponse.message(),
+                    tapoResponse.errorCode());
+            throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+        }
+    }
+
+    /**
+     * query Token from device with encoded credentials
+     */
+    private void createToken(TapoCredentials encodedCredentials)
+            throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+        /* create handshake request */
+        JsonObject json = new JsonObject();
+        json.addProperty("username", encodedCredentials.username());
+        json.addProperty("password", encodedCredentials.password());
+        TapoRequest loginRequest = new TapoRequest(DEVICE_CMD_GET_TOKEN, json);
+
+        /* send request */
+        ContentResponse response = sendHandshakeRequest(loginRequest, true);
+        handleTokenResponse(response);
+    }
+
+    /**
+     * get token from "login"-request
+     */
+    private void handleTokenResponse(ContentResponse response) throws TapoErrorHandler {
+        TapoResponse tapoResponse = spth.getTapoResponse(response);
+        if (!tapoResponse.hasError()) {
+            setToken(jsonObjectToString(tapoResponse.result(), "token"));
+        } else {
+            logger.debug("({}) invalid response while login: {} ({})", uid, tapoResponse.message(),
+                    tapoResponse.errorCode());
+            throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+        }
+    }
+
+    private void setToken(String token) {
+        this.token = token;
+    }
+
+    private void unsetToken() {
+        token = "";
+    }
+
+    /**
+     * Cookie Handling
+     */
+    private void setCookie(String cookie) {
+        this.cookie = cookie;
+    }
+
+    private void unsetCookie() {
+        spth.httpDelegator.getHttpClient().getCookieStore().removeAll();
+        this.cookie = "";
+    }
+
+    /************************
+     * GET VALUES
+     ************************/
+
+    public String getCookie() {
+        return cookie;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public SecurePasstroughCipher getCipher() {
+        return cipher;
+    }
+
+    public boolean isHandshakeComplete() {
+        return !cookie.isBlank() && !token.isBlank();
+    }
+
+    /***********************************
+     * ENCRYPTION / CODING
+     ************************************/
+
+    private TapoCredentials encodeCredentials(TapoCredentials credentials) throws NoSuchAlgorithmException {
+        String username = TapoEncoder.b64Encode(TapoEncoder.sha1Encode(credentials.username()));
+        String password = TapoEncoder.b64Encode(credentials.password());
+        return new TapoCredentials(username, password);
+    }
+
+    /**
+     * Decrypt encrypted TapoResponse
+     */
+    public TapoResponse decryptResponse(TapoResponse response) throws TapoErrorHandler {
+        if (response.result().has("response")) {
+            try {
+                String encryptedResponse = response.result().get("response").getAsString();
+                String decryptedResponse = cipher.decode(encryptedResponse);
+                logger.trace("({}) decrypted response '{}'", uid, decryptedResponse);
+                return Objects.requireNonNull(GSON.fromJson(decryptedResponse, TapoResponse.class));
+            } catch (Exception e) {
+                logger.debug("({}) exception '{}' decryptingResponse: '{}'", uid, e, response);
+                throw new TapoErrorHandler(ERR_DATA_DECRYPTING);
+            }
+        }
+        throw new TapoErrorHandler(ERR_DATA_FORMAT);
+    }
+
+    /**
+     * Encrypt Request
+     */
+    public TapoRequest encryptRequest(Object request) throws TapoErrorHandler {
+        try {
+            JsonObject jso = new JsonObject();
+            jso.addProperty("request", cipher.encode(GSON.toJson(request)));
+            return new TapoRequest(DEVICE_CMD_SECURE_METHOD, jso);
+        } catch (Exception e) {
+            logger.debug("({}) exception encoding Payload '{}'", uid, e.toString());
+            throw new TapoErrorHandler(ERR_DATA_ENCRYPTING);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePasstroughCipher.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePasstroughCipher.java
new file mode 100644 (file)
index 0000000..0e6f54f
--- /dev/null
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.aes;
+
+import static java.util.Base64.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoKeyPair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO-CIPHER
+ * Based on K4CZP3R's p100-java-poc
+ * 
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class SecurePasstroughCipher {
+    private final Logger logger = LoggerFactory.getLogger(SecurePasstroughCipher.class);
+    protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+    protected static final String CIPHER_ALGORITHM = "AES";
+    protected static final String CIPHER_CHARSET = "UTF-8";
+    protected static final String HANDSHAKE_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
+    protected static final String HANDSHAKE_ALGORITHM = "RSA";
+    protected static final String HANDSHAKE_CHARSET = "UTF-8";
+
+    @NonNullByDefault({})
+    private Cipher encodeCipher;
+    @NonNullByDefault({})
+    private Cipher decodeCipher;
+
+    /**
+     * CREATE NEW EMPTY CIPHER
+     */
+    public SecurePasstroughCipher() {
+    }
+
+    /**
+     * CREATE NEW CIPHER WITH KEY AND CREDENTIALS
+     * 
+     * @param handshakeKey key from Handshake-Request
+     * @param keyPair keyPair
+     * @throws TapoErrorHandler
+     */
+    public SecurePasstroughCipher(String handshakeKey, TapoKeyPair keyPair) throws TapoErrorHandler {
+        setKey(handshakeKey, keyPair);
+    }
+
+    /**
+     * SET NEW KEY AND CREDENTIALS
+     * 
+     * @param handshakeKey key from Handshake-Request
+     * @param keyPair keyPair
+     */
+    public void setKey(String handshakeKey, TapoKeyPair keyPair) throws TapoErrorHandler {
+        logger.trace("Init passtroughCipher with key: {} ", handshakeKey);
+        try {
+            byte[] decode = getMimeDecoder().decode(handshakeKey.getBytes(HANDSHAKE_CHARSET));
+            byte[] decode2 = getMimeDecoder().decode(keyPair.getPrivateKeyBytes());
+            Cipher instance = Cipher.getInstance(HANDSHAKE_TRANSFORMATION);
+            KeyFactory kf = KeyFactory.getInstance(HANDSHAKE_ALGORITHM);
+            PrivateKey p = kf.generatePrivate(new PKCS8EncodedKeySpec(decode2));
+            instance.init(Cipher.DECRYPT_MODE, p);
+            byte[] doFinal = instance.doFinal(decode);
+            byte[] bArr = new byte[16];
+            byte[] bArr2 = new byte[16];
+            System.arraycopy(doFinal, 0, bArr, 0, 16);
+            System.arraycopy(doFinal, 16, bArr2, 0, 16);
+            initCipher(bArr, bArr2);
+        } catch (Exception e) {
+            logger.warn("handshake Failed: {}", e.getMessage());
+            throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, e.getMessage());
+        }
+    }
+
+    /**
+     * INIT ENCODE/DECDE-CIPHERS
+     * 
+     * @param bArr
+     * @param bArr2
+     * @throws Exception
+     */
+    protected void initCipher(byte[] bArr, byte[] bArr2) throws Exception {
+        try {
+            SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, CIPHER_ALGORITHM);
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
+            encodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            decodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            encodeCipher.init(1, secretKeySpec, ivParameterSpec);
+            decodeCipher.init(2, secretKeySpec, ivParameterSpec);
+        } catch (Exception e) {
+            logger.warn("initChiper failed: {}", e.getMessage());
+            encodeCipher = null;
+            decodeCipher = null;
+        }
+    }
+
+    /**
+     * ENCODE STRING
+     * 
+     * @param str source string to encode
+     * @return encoded string
+     * @throws Exception
+     */
+    public String encode(String str) throws Exception {
+        byte[] doFinal;
+        doFinal = encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET));
+        String encrypted = getMimeEncoder().encodeToString(doFinal);
+        return encrypted.replace("\r\n", "");
+    }
+
+    /**
+     * DECODE STRING
+     * 
+     * @param str source string to decode
+     * @return decoded string
+     * @throws Exception
+     */
+    public String decode(String str) throws Exception {
+        byte[] data = getMimeDecoder().decode(str.getBytes(CIPHER_CHARSET));
+        byte[] doFinal;
+        doFinal = decodeCipher.doFinal(data);
+        return new String(doFinal);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapCipher.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapCipher.java
new file mode 100644 (file)
index 0000000..44e981a
--- /dev/null
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.klap;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * Cipher for KLAP-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class KlapCipher {
+    protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+    protected static final String CIPHER_ALGORITHM = "AES";
+    protected static final String CIPHER_CHARSET = "UTF-8";
+
+    private int ivSeq = 0;
+    private byte[] iv;
+    private byte[] key;
+    private byte[] sig;
+
+    /************************
+     * INIT CLASS
+     ************************/
+
+    /**
+     * Init KlapCipher with seeds generated by session and got from device
+     * 
+     * @param localSeed local random 16 byte seedhash sent to device
+     * @param remoteSeed 16 byte seed received as awnser from device
+     * @param userHash sha256 hash of user-credentials
+     */
+    public KlapCipher(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+            throws TapoErrorHandler, NoSuchAlgorithmException {
+        iv = initIvDerive(localSeed, remoteSeed, userHash);
+        key = initKeyDerive(localSeed, remoteSeed, userHash);
+        sig = initSigDerive(localSeed, remoteSeed, userHash);
+    }
+
+    /**
+     * reset all data (logout)
+     */
+    public void reset() {
+        ivSeq = 0;
+        iv = new byte[0];
+        key = new byte[0];
+        sig = new byte[0];
+    }
+
+    /************************
+     * GET VALUES
+     ************************/
+
+    /**
+     * return true if cipher is fully initialized (handshakes and cookies completed)
+     */
+    public boolean isInitialized() {
+        return iv.length > 0 && key.length > 0 && sig.length > 0;
+    }
+
+    /**
+     * get iv-sequence number which increments every request
+     */
+    public int getIvSeq() {
+        return ivSeq;
+    }
+
+    /************************
+     * ENCODING / DECODING
+     ************************/
+
+    /**
+     * ENCRYPT STRING TO BYTEARRAY
+     */
+    public byte[] encrypt(String str) throws TapoErrorHandler {
+        try {
+            ivSeq += 1; /* increment sequence */
+
+            Cipher encodeCipher = getCipher(Cipher.ENCRYPT_MODE, ivSeq);
+            byte[] msg = str.getBytes(CIPHER_CHARSET);
+            byte[] cipherText = encodeCipher.doFinal(msg);
+            byte[] signature = sha256Encode(concatBytes(sig, BigInteger.valueOf(ivSeq).toByteArray(), cipherText));
+            return concatBytes(signature, cipherText);
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_DATA_ENCRYPTING, e.getMessage());
+        }
+    }
+
+    /**
+     * DECRYPT BYTEARRAY INTO STRING
+     */
+    public String decrypt(byte[] byteArr, int ivSeq) throws TapoErrorHandler {
+        try {
+            Cipher decodeCipher = getCipher(Cipher.DECRYPT_MODE, ivSeq);
+            byte[] bytesToDecode = truncateByteArray(byteArr, 32, byteArr.length - 32);
+            byte[] doFinal = decodeCipher.doFinal(bytesToDecode);
+            return new String(doFinal);
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_DATA_DECRYPTING, e.getMessage());
+        }
+    }
+
+    /**
+     * get iv-buffer
+     * 
+     * @param ivSeq ivSequence-Number
+     */
+    private byte[] getIv(int ivSeq) throws TapoErrorHandler {
+        byte[] seq = BigInteger.valueOf(ivSeq).toByteArray();
+        return concatBytes(iv, seq);
+    }
+
+    /************************
+     * INIT CIPHER-SPECS
+     ************************/
+
+    /**
+     * INIT IV-DERIVE FROM SEEDS
+     */
+    private byte[] initIvDerive(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+            throws TapoErrorHandler, NoSuchAlgorithmException {
+        /* iv is first 16 bytes of sha256, where the last 4 bytes forms the */
+        /* sequence number used in requests and is incremented on each request */
+        byte[] fullBytes = concatBytes("iv".getBytes(), localSeed, remoteSeed, userHash);
+        byte[] byteHash = sha256Encode(fullBytes);
+        byte[] seqBytes = truncateByteArray(byteHash, byteHash.length - 4, 4);
+
+        ivSeq = new BigInteger(seqBytes).intValue(); /* get sequence number */
+        return truncateByteArray(byteHash, 0, 12);
+    }
+
+    /**
+     * INIT KEY-DERIVE FROM SEEDS
+     */
+    private byte[] initKeyDerive(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+            throws TapoErrorHandler, NoSuchAlgorithmException {
+        byte[] fullBytes = concatBytes("lsk".getBytes(), localSeed, remoteSeed, userHash);
+        byte[] byteHash = sha256Encode(fullBytes);
+        return truncateByteArray(byteHash, 0, 16);
+    }
+
+    /**
+     * INIT SIGNATURE-DERIVE FROM SEEDS
+     */
+    private byte[] initSigDerive(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+            throws TapoErrorHandler, NoSuchAlgorithmException {
+        /* used to create a hash with which to prefix each request */
+        byte[] fullBytes = concatBytes("ldk".getBytes(), localSeed, remoteSeed, userHash);
+        byte[] byteHash = sha256Encode(fullBytes);
+        return truncateByteArray(byteHash, 0, 28);
+    }
+
+    /************************
+     * HELPERS
+     ************************/
+
+    /**
+     * Return initialized Cipher
+     * 
+     * @param opMode op-mode (encrypt/decrypt)
+     * @param ivSeq iv-sequence number
+     */
+    private Cipher getCipher(int opMode, int ivSeq) throws TapoErrorHandler {
+        try {
+            byte[] ivBuffer = getIv(ivSeq);
+            SecretKeySpec secretKeySpec = new SecretKeySpec(key, CIPHER_ALGORITHM);
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBuffer);
+
+            Cipher myCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            myCipher.init(opMode, secretKeySpec, ivParameterSpec);
+
+            return myCipher;
+        } catch (Exception e) {
+            throw new TapoErrorHandler(e);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapProtocol.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapProtocol.java
new file mode 100644 (file)
index 0000000..4e3c59b
--- /dev/null
@@ -0,0 +1,274 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.klap;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO-KLAP-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class KlapProtocol implements org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface {
+
+    private final Logger logger = LoggerFactory.getLogger(KlapProtocol.class);
+    protected final TapoConnectorInterface httpDelegator;
+    private KlapSession session;
+    private String uid;
+
+    /***********************
+     * Init Class
+     **********************/
+    public KlapProtocol(TapoConnectorInterface httpDelegator) {
+        this.httpDelegator = httpDelegator;
+        session = new KlapSession(this);
+        uid = httpDelegator.getThingUID() + " / HTTP-KLAP";
+    }
+
+    @Override
+    public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
+        logger.trace("({}) login to device", uid);
+        session.reset();
+        session.login(tapoCredentials);
+        return isLoggedIn();
+    }
+
+    @Override
+    public void logout() {
+        session.reset();
+    }
+
+    @Override
+    public boolean isLoggedIn() {
+        return session.isHandshakeComplete() && session.seedIsOkay() && !session.isExpired();
+    }
+
+    /***********************
+     * Request Sender
+     **********************/
+
+    /*
+     * send synchron request - response will be handled in [responseReceived()] function
+     */
+    @Override
+    public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler {
+        String url = getUrl();
+        String command = tapoRequest.method();
+        logger.trace("({}) sending unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+
+        Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header */
+        httpRequest = setHeaders(httpRequest);
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        /* add request body */
+        httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+        try {
+            responseReceived(httpRequest.send(), command);
+        } catch (Exception e) {
+            throw new TapoErrorHandler(e, "error sending content");
+        }
+    }
+
+    /**
+     * handle asynchron request-response
+     * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
+        String url = getUrl();
+        String command = tapoRequest.method();
+        logger.trace("({}) sendAsync unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+
+        /* encrypt request */
+        byte[] encodedBytes = session.encryptRequest(tapoRequest);
+        String encrypteString = byteArrayToHex(encodedBytes);
+        Integer ivSequence = session.getIvSequence();
+        logger.trace("({}) encrypted request is '{}' with sequence '{}'", uid, encrypteString, ivSequence);
+
+        Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header and params */
+        httpRequest = setHeaders(httpRequest);
+        httpRequest.param("seq", ivSequence.toString());
+
+        /* add request body */
+        httpRequest.content(new BytesContentProvider(encodedBytes));
+
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
+            @NonNullByDefault({})
+            @Override
+            public void onComplete(Result result) {
+                final HttpResponse response = (HttpResponse) result.getResponse();
+
+                if (result.getFailure() != null) {
+                    /* handle result errors */
+                    Throwable e = result.getFailure();
+                    String errorMessage = getValueOrDefault(e.getMessage(), "");
+                    /* throw errors to delegator */
+                    if (e instanceof TimeoutException) {
+                        logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
+                        httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
+                    } else {
+                        logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
+                        httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
+                    }
+                } else if (response.getStatus() != 200) {
+                    logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
+                    httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
+                } else {
+                    /* request successful */
+                    byte[] responseBytes = getContent();
+                    try {
+                        encryptedResponseReceived(responseBytes, ivSequence, command);
+                    } catch (TapoErrorHandler tapoError) {
+                        httpDelegator.handleError(tapoError);
+                    }
+                }
+            }
+        });
+    }
+
+    /************************
+     * RESPONSE HANDLERS
+     ************************/
+
+    /**
+     * handle synchron request-response
+     * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
+        logger.trace("({}) received response content: '{}'", uid, response.getContentAsString());
+        TapoResponse tapoResponse = getTapoResponse(response);
+        httpDelegator.handleResponse(tapoResponse, command);
+        httpDelegator.responsePasstrough(response.getContentAsString(), command);
+    }
+
+    /**
+     * handle asynchron request-response
+     * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
+        try {
+            TapoResponse tapoResponse = getTapoResponse(content);
+            httpDelegator.handleResponse(tapoResponse, command);
+        } catch (TapoErrorHandler tapoError) {
+            httpDelegator.handleError(tapoError);
+        }
+    }
+
+    /**
+     * handle encrypted response. decrypt it and pass to asyncRequestReceived
+     * 
+     * @param content bytearray with encrypted payload
+     * @param ivSeq ivSequence-Number which is incremented each request
+     * @param command command was sent to device
+     * @throws TapoErrorHandler
+     */
+    public void encryptedResponseReceived(byte[] content, Integer ivSeq, String command) throws TapoErrorHandler {
+        String stringContent = byteArrayToHex(content);
+        logger.trace("({}) receivedRespose '{}'", uid, stringContent);
+        String decryptedResponse = session.decryptResponse(content, ivSeq);
+        logger.trace("({}) decrypted response: '{}'", uid, decryptedResponse);
+        asyncResponseReceived(decryptedResponse, command);
+    }
+
+    /**
+     * Get Tapo-Response from Contentresponse
+     * decrypt if is encrypted
+     */
+    protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
+        if (response.getStatus() == 200) {
+            return getTapoResponse(response.getContentAsString());
+        } else {
+            logger.debug("({}) invalid response received", uid);
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /**
+     * Get Tapo-Response from responsestring
+     * decrypt if is encrypted
+     */
+    protected TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
+        if (isValidJson(responseString)) {
+            TapoResponse tapoResponse = Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
+            if (tapoResponse.hasError()) {
+                throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+            }
+            return tapoResponse;
+        } else {
+            logger.debug("({}) invalid response received", uid);
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /************************
+     * PRIVATE HELPERS
+     ************************/
+
+    protected String getUrl() {
+        String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
+        if (session.isHandshakeComplete()) {
+            return baseUrl + "/request";
+        } else {
+            return baseUrl;
+        }
+    }
+
+    /*
+     * Set HTTP-Headers
+     */
+    protected Request setHeaders(Request httpRequest) {
+        if (!session.isHandshakeComplete()) {
+            httpRequest.header("content-type", CONTENT_TYPE_JSON);
+            httpRequest.header("Accept", CONTENT_TYPE_JSON);
+        }
+        if (!session.getCookie().isBlank()) {
+            httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());
+        }
+        return httpRequest;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapSession.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapSession.java
new file mode 100644 (file)
index 0000000..dfaba69
--- /dev/null
@@ -0,0 +1,323 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.klap;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+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.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for KLAP Session
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class KlapSession {
+    private final Logger logger = LoggerFactory.getLogger(KlapSession.class);
+
+    private final KlapProtocol klap;
+    private @NonNullByDefault({}) KlapCipher cipher;
+    private byte[] localSeed = new byte[16];
+    private byte[] remoteSeed = new byte[16];
+    private byte[] serverHash = new byte[0];
+    private byte[] authHash = new byte[0];
+    private byte[] localSeedAuthHash = new byte[0];
+    private String sessionId = "";
+    private long expireAt = 0L;
+    private String uid;
+
+    // List of class-specific commands
+    public static final String DEVICE_CMD_HANDSHAKE1 = "handshake1";
+    public static final String DEVICE_CMD_HANDSHAKE2 = "handshake2";
+    public static final String DEVICE_CMD_SECURE_METHOD = "securePassthrough";
+    public static final String TP_SESSION_COOKIE_HEADER = "Set-Cookie";
+    public static final String TP_SESSION_COOKIE_NAME = "TP_SESSIONID";
+    public static final String TP_SESSION_COOKIE_TIMEOUT = "TIMEOUT";
+
+    /************************
+     * INIT CLASS
+     ************************/
+
+    public KlapSession(KlapProtocol klapHandler) {
+        klap = klapHandler;
+        localSeed = newLocalSeed();
+        uid = klap.httpDelegator.getThingUID() + "KLAP-Session";
+    }
+
+    /* create new random local seed */
+    private byte[] newLocalSeed() {
+        SecureRandom random = new SecureRandom();
+        byte[] randomBytes = new byte[16];
+        random.nextBytes(randomBytes);
+        return randomBytes;
+    }
+
+    /************************
+     * SET VALUES
+     ************************/
+
+    /* reset data (logout) */
+    public void reset() {
+        logger.trace("reset KlapSession");
+        localSeed = newLocalSeed();
+        remoteSeed = new byte[0];
+        localSeedAuthHash = new byte[0];
+        serverHash = new byte[0];
+        sessionId = "";
+        expireAt = 0L;
+        cipher = null;
+    }
+
+    /* set sessionId (cookie) */
+    public boolean setSession(ContentResponse response) throws TapoErrorHandler {
+        try {
+            /* get cookie */
+            String header = response.getHeaders().get(TP_SESSION_COOKIE_HEADER);
+            String cookie = header.split(";")[0].replace(TP_SESSION_COOKIE_NAME + "=", "");
+            int timeout = Integer.parseInt(header.split(";")[1].replace(TP_SESSION_COOKIE_TIMEOUT + "=", ""));
+            sessionId = cookie;
+            expireAt = System.currentTimeMillis() * timeout;
+            /* get seeds */
+            byte[] responseContent = response.getContent();
+            byte[] bRemoteSeed = truncateByteArray(responseContent, 0, 16); // get first 16 bytes
+            byte[] bServerHash = truncateByteArray(responseContent, 16, responseContent.length - 16); // get
+                                                                                                      // rest
+            setSeed(bRemoteSeed, bServerHash);
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, e.getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * set Seed and compare handshake1 hashes
+     */
+    public boolean setSeed(byte[] remoteSeed, byte[] serverHash) throws TapoErrorHandler {
+        logger.trace("({}) Init Session", uid);
+        try {
+            logger.trace("remoteseed is '{}' / serverhash is '{}' authhash is '{}'", byteArrayToHex(localSeed),
+                    byteArrayToHex(remoteSeed), byteArrayToHex(authHash));
+
+            byte[] concatByteArray = concatBytes(localSeed, remoteSeed, authHash);
+            byte[] bLocalSeedAuthHash = sha256Encode(concatByteArray);
+
+            if (Arrays.equals(bLocalSeedAuthHash, serverHash)) {
+                logger.trace("handshake1 sucessfull");
+                this.remoteSeed = remoteSeed;
+                this.serverHash = serverHash;
+                this.localSeedAuthHash = bLocalSeedAuthHash;
+                return true;
+            } else {
+                logger.trace("handshake1 does not match {} / {}", byteArrayToHex(bLocalSeedAuthHash),
+                        byteArrayToHex(serverHash));
+                reset();
+                throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED);
+            }
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED);
+        }
+    }
+
+    /***********************
+     * Request Sender
+     **********************/
+
+    /*
+     * Sender for handling Handshake
+     */
+    private ContentResponse sendHandshake(String customUrl, byte[] payloadBytes, String command)
+            throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+        logger.trace("({}) sending bytes'{} to '{}' ", uid, byteArrayToHex(payloadBytes), customUrl);
+
+        Request httpRequest = klap.httpDelegator.getHttpClient().newRequest(customUrl)
+                .method(HttpMethod.POST.toString());
+
+        httpRequest = klap.setHeaders(httpRequest);
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        /* add request body */
+        httpRequest.content(new BytesContentProvider(payloadBytes));
+
+        return httpRequest.send();
+    }
+
+    /* complete Handshake (init Cipher) */
+    private void completeHandshake() throws Exception {
+        cipher = new KlapCipher(localSeed, remoteSeed, authHash);
+    }
+
+    /************************
+     * HANDSHAKE & COOKIE
+     ************************/
+
+    /**
+     * Create Handshake
+     */
+    public boolean login(TapoCredentials credentials) throws TapoErrorHandler {
+        try {
+            authHash = generateAuthHash(credentials);
+            if (createHandshake1()) {
+                logger.trace("({}) handshake1 successfull", uid);
+                if (createHandshake2()) {
+                    logger.trace("({}) handshake2 successfull", uid);
+                    completeHandshake();
+                    return isHandshakeComplete();
+                }
+            } else {
+                throw new TapoErrorHandler(NO_ERROR, BINDING_ID);
+            }
+        } catch (TapoErrorHandler e) {
+            throw e;
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED);
+        }
+        return false;
+    }
+
+    /**
+     * Handle Handshake (set session)
+     */
+    private boolean createHandshake1()
+            throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+        String handshakeUrl = klap.getUrl() + "/" + DEVICE_CMD_HANDSHAKE1;
+        byte[] bytes = getLocalSeed();
+        ContentResponse response = sendHandshake(handshakeUrl, bytes, DEVICE_CMD_HANDSHAKE1);
+        if (response.getStatus() == 200) {
+            logger.trace("({}) got handshake response", uid);
+            setSession(response);
+            return seedIsOkay();
+        } else {
+            logger.debug("({}) invalid handshake1 response {}", uid, response.getStatus());
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /**
+     * Complete Handshake2 (set cipher)
+     */
+    private boolean createHandshake2() throws TimeoutException, InterruptedException, ExecutionException,
+            NoSuchAlgorithmException, TapoErrorHandler {
+        String handshakeUrl = klap.getUrl() + "/" + DEVICE_CMD_HANDSHAKE2;
+        byte[] byteArr = concatBytes(remoteSeed, localSeed, authHash);
+        byte[] payloadBytes = sha256Encode(byteArr);
+        ContentResponse response = sendHandshake(handshakeUrl, payloadBytes, DEVICE_CMD_HANDSHAKE2);
+
+        if (response.getStatus() == 200) {
+            return true;
+        } else {
+            logger.debug("({}) invalid handshake1 response {}", uid, response.getStatus());
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, response.getReason());
+        }
+    }
+
+    /************************
+     * ENCODING / DECODING
+     ************************/
+
+    /**
+     * generate auth-hash from credentials
+     */
+    private byte[] generateAuthHash(TapoCredentials credentials) throws NoSuchAlgorithmException, TapoErrorHandler {
+        byte[] bUsername = sha1Encode(credentials.username().getBytes(StandardCharsets.UTF_8));
+        byte[] bPassword = sha1Encode(credentials.password().getBytes(StandardCharsets.UTF_8));
+        return sha256Encode(concatBytes(bUsername, bPassword));
+    }
+
+    /**
+     * Decrypt encrypted TapoResponse
+     */
+    public String decryptResponse(byte[] byteResponse, Integer ivSeq) throws TapoErrorHandler {
+        try {
+            return cipher.decrypt(byteResponse, ivSeq);
+        } catch (Exception e) {
+            logger.debug("({}) exception decrypting Payload '{}'", uid, e.toString());
+            throw new TapoErrorHandler(ERR_DATA_DECRYPTING);
+        }
+    }
+
+    /**
+     * Encrypt Request
+     */
+    public byte[] encryptRequest(Object request) throws TapoErrorHandler {
+        try {
+            return cipher.encrypt(request.toString());
+        } catch (Exception e) {
+            logger.debug("({}) exception encrypting Payload '{}'", uid, e.toString());
+            throw new TapoErrorHandler(ERR_DATA_ENCRYPTING);
+        }
+    }
+
+    /************************
+     * GET VALUES
+     ************************/
+
+    public long expireAt() {
+        return expireAt;
+    }
+
+    public String getCookie() {
+        if (!sessionId.isBlank()) {
+            return TP_SESSION_COOKIE_NAME + "=" + sessionId;
+        }
+        return "";
+    }
+
+    public KlapCipher getCipher() {
+        return cipher;
+    }
+
+    public byte[] getLocalSeed() {
+        return localSeed;
+    }
+
+    public Integer getIvSequence() {
+        return cipher.getIvSeq();
+    }
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public boolean isHandshakeComplete() {
+        return !sessionId.isBlank() && cipher != null && cipher.isInitialized();
+    }
+
+    public boolean isExpired() {
+        return (expireAt - (System.currentTimeMillis())) <= 40 * 1000;
+    }
+
+    /* return true if seeds are set */
+    public boolean seedIsOkay() {
+        return serverHash.length > 0 && Arrays.equals(localSeedAuthHash, serverHash);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/passthrough/PassthroughProtocol.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/passthrough/PassthroughProtocol.java
new file mode 100644 (file)
index 0000000..77b05f8
--- /dev/null
@@ -0,0 +1,257 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.api.protocol.passthrough;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.Objects;
+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.HttpResponse;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO-PASSTHROUGH-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class PassthroughProtocol implements TapoProtocolInterface {
+    private final Logger logger = LoggerFactory.getLogger(PassthroughProtocol.class);
+    private final TapoConnectorInterface httpDelegator;
+    private final String uid;
+
+    /***********************
+     * Init Class
+     **********************/
+
+    public PassthroughProtocol(TapoConnectorInterface httpDelegator) {
+        this.httpDelegator = httpDelegator;
+        uid = httpDelegator.getThingUID() + " / HTTP-Passtrhough";
+    }
+
+    /***********************
+     * Login Handling
+     **********************/
+
+    @Override
+    public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
+        logger.debug("({}) login not implemented", uid);
+        throw new TapoErrorHandler(ERR_BINDING_NOT_IMPLEMENTED, "NOT NEEDED");
+    }
+
+    @Override
+    public void logout() {
+        logger.trace("({}) logout not implemented", uid);
+    }
+
+    @Override
+    public boolean isLoggedIn() {
+        logger.debug("({}) isLoggedIn not implemented", uid);
+        return false;
+    }
+
+    /***********************
+     * Request Sender
+     **********************/
+
+    /*
+     * send synchronous request - response will be handled in [responseReceived()] function
+     */
+    @Override
+    public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler {
+        String url = getUrl();
+        logger.trace("({}) sending encrypted request to '{}' ", uid, url);
+        logger.trace("({}) unencrypted request: '{}'", uid, tapoRequest);
+
+        Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header */
+        httpRequest = setHeaders(httpRequest);
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        /* add request body */
+        httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+        try {
+            responseReceived(httpRequest.send(), tapoRequest.method());
+        } catch (TapoErrorHandler tapoError) {
+            logger.debug("({}) sendRequest exception'{}'", uid, tapoError.toString());
+            throw tapoError;
+        } catch (TimeoutException e) {
+            logger.debug("({}) sendRequest timeout'{}'", uid, e.getMessage());
+            throw new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, getValueOrDefault(e.getMessage(), ""));
+        } catch (InterruptedException e) {
+            logger.debug("({}) sendRequest interrupted'{}'", uid, e.getMessage());
+            throw new TapoErrorHandler(ERR_BINDING_SEND_REQUEST, getValueOrDefault(e.getMessage(), ""));
+        } catch (ExecutionException e) {
+            logger.debug("({}) sendRequest exception'{}'", uid, e.getMessage());
+            throw new TapoErrorHandler(ERR_BINDING_SEND_REQUEST, getValueOrDefault(e.getMessage(), ""));
+        }
+    }
+
+    /*
+     * send asynchronous request - response will be handled in [asyncResponseReceived()] function
+     */
+    @Override
+    public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
+        String url = getUrl();
+        String command = tapoRequest.method();
+        logger.trace("({}) sendAsncRequest to '{}'", uid, url);
+        logger.trace("({}) command/payload: '{}''{}'", uid, command, tapoRequest.params());
+
+        Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header */
+        httpRequest = setHeaders(httpRequest);
+
+        /* add request body */
+        httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+        httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
+            @NonNullByDefault({})
+            @Override
+            public void onComplete(Result result) {
+                final HttpResponse response = (HttpResponse) result.getResponse();
+                if (result.getFailure() != null) {
+                    /* handle result errors */
+                    Throwable e = result.getFailure();
+                    String errorMessage = getValueOrDefault(e.getMessage(), "");
+                    if (e instanceof TimeoutException) {
+                        logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
+                        httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
+                    } else {
+                        logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
+                        httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
+                    }
+                } else if (response.getStatus() != 200) {
+                    logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
+                    httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
+                } else {
+                    /* request successful */
+                    String rBody = getContentAsString();
+                    try {
+                        asyncResponseReceived(rBody, command);
+                    } catch (TapoErrorHandler tapoError) {
+                        httpDelegator.handleError(tapoError);
+                    }
+                }
+            }
+        });
+    }
+
+    /************************
+     * RESPONSE HANDLERS
+     ************************/
+
+    /**
+     * handle synchronous request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
+        logger.trace("({}) recived response: {}", uid, response.getContentAsString());
+        TapoResponse tapoResponse = getTapoResponse(response);
+        if (!tapoResponse.hasError()) {
+            switch (command) {
+                default:
+                    httpDelegator.handleResponse(tapoResponse, command);
+                    httpDelegator.responsePasstrough(response.getContentAsString(), command);
+            }
+        } else {
+            logger.debug("({}) response returned error: {} ({})", uid, tapoResponse.message(),
+                    tapoResponse.errorCode());
+            httpDelegator.handleError(new TapoErrorHandler(tapoResponse.errorCode()));
+        }
+    }
+
+    /**
+     * handle asynchronous request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function
+     */
+    @Override
+    public void asyncResponseReceived(String responseBody, String command) throws TapoErrorHandler {
+        logger.trace("({}) asyncResponseReceived '{}'", uid, responseBody);
+        TapoResponse tapoResponse = getTapoResponse(responseBody);
+        if (!tapoResponse.hasError()) {
+            httpDelegator.handleResponse(tapoResponse, command);
+        } else {
+            httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, command));
+        }
+    }
+
+    /************************
+     * 
+     * PRIVATE HELPERS
+     * 
+     ************************/
+
+    /**
+     * Get Tapo-Response from Contentresponse
+     */
+    private TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
+        if (response.getStatus() == 200) {
+            return getTapoResponse(response.getContentAsString());
+        } else {
+            logger.debug("({}) invalid response received", uid);
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /**
+     * Get Tapo-Response from responsestring
+     */
+    private TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
+        if (isValidJson(responseString)) {
+            return Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
+        } else {
+            logger.debug("({}) invalid response received", uid);
+            throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+        }
+    }
+
+    /**
+     * Get Url requests are sent to
+     */
+    public String getUrl() {
+        return httpDelegator.getBaseUrl();
+    }
+
+    /*
+     * Set HTTP-Headers
+     */
+    public Request setHeaders(Request httpRequest) {
+        httpRequest.header("content-type", CONTENT_TYPE_JSON);
+        httpRequest.header("Accept", CONTENT_TYPE_JSON);
+        return httpRequest;
+    }
+}
index 79d098f873577f4918a012b4615a38a922332d8e..6ed25d5c71458b342f2ef725c3349d42a7f7c333 100644 (file)
@@ -38,24 +38,15 @@ public class TapoBindingSettings {
     public static final Integer TAPO_HTTP_TIMEOUT_MS = 5000; // http request timeout
     public static final Integer TAPO_HTTP_CLOUD_TIMEOUT_MS = 10000; // http request cloud timeout
     public static final Integer TAPO_PING_TIMEOUT_MS = 2000; // ping timeout
-    public static final Integer TAPO_REFRESH_MIN_GAP_MS = 5000; // min gap between sending refresh request
+    public static final Integer TAPO_QUERY_MIN_GAP_MS = 1000; // min gap between sending query request
     public static final Integer TAPO_SEND_MIN_GAP_MS = 1000; // min gap between sending command request
     public static final Integer TAPO_LOGIN_MIN_GAP_MS = 5000; // min gap between sending login request
     public static final Integer TAPO_LOGIN_MAX_GAP_M = 1440; // max minutes to relogin to device
-    public static final Integer TAPO_DISCOVERY_TIMEOUT_S = 6; // timout device discovery in seconds
-    public static final Integer POLLING_MIN_INTERVAL_S = 10; // min polling interval (settings)
+    public static final Integer TAPO_DISCOVERY_TIMEOUT_S = 20; // timout device discovery in seconds
+    public static final Integer POLLING_MIN_INTERVAL_S = 1; // min polling interval (settings)
+    public static final Integer TAPO_MULTI_COMMAND_OFFSET_MS = 100; // Offset between sending multiple commands in ms
 
     // FORMATING CONSTANTS
     public static final String IPV4_REGEX = "(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))";
     public static final char MAC_DIVISION_CHAR = '-';
-
-    // LIST OF DEVICE-COMMANDS
-    public static final String DEVICE_CMD_GETINFO = "get_device_info";
-    public static final String DEVICE_CMD_SETINFO = "set_device_info";
-    public static final String DEVICE_CMD_GETENERGY = "get_energy_usage";
-    public static final String DEVICE_CMD_CHILD_DEVICE_LIST = "get_child_device_list";
-    public static final String DEVICE_CMD_CONTROL_CHILD = "control_child";
-    public static final String DEVICE_CMD_MULTIPLE_REQ = "multipleRequest";
-    public static final String DEVICE_CMD_CUSTOM = "custom_command";
-    public static final String DEVICE_CMD_SET_LIGHT_FX = "set_dynamic_light_effect_rule_enable";
 }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoComConstants.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoComConstants.java
new file mode 100644 (file)
index 0000000..f72b902
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.constants;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TapoComConstants} class defines communication constants, which are
+ * used across the whole binding.
+ *
+ * @author Christian Wild - Initial contribution
+ ***/
+@NonNullByDefault
+public class TapoComConstants {
+    // DEVICE JSON STRINGS
+    public static final String JSON_KEY_LIGHTNING_EFFECT_OFF = "off";
+    public static final String DEVICE_REPRESENTATION_PROPERTY = "macAddress";
+
+    // List of Cloud-Commands
+    public static final String CLOUD_CMD_LOGIN = "login";
+    public static final String CLOUD_CMD_GETDEVICES = "getDeviceList";
+
+    // List of Device-Control-Commands
+    public static final String DEVICE_CMD_GETINFO = "get_device_info";
+    public static final String DEVICE_CMD_SETINFO = "set_device_info";
+    public static final String DEVICE_CMD_GETENERGY = "get_energy_usage";
+    public static final String DEVICE_CMD_GETCHILDDEVICELIST = "get_child_device_list";
+    public static final String DEVICE_CMD_CONTROL_CHILD = "control_child";
+    public static final String DEVICE_CMD_MULTIPLE_REQ = "multipleRequest";
+    public static final String DEVICE_CMD_CUSTOM = "custom_command";
+    public static final String DEVICE_CMD_SET_DYNAIMCLIGHT_FX = "set_dynamic_light_effect_rule_enable";
+    public static final String DEVICE_CMD_SET_LIGHT_FX = "set_lighting_effect";
+
+    // Sets
+    public static final Set<String> DEVICE_CMDLIST_QUERY = Set.of(DEVICE_CMD_GETINFO, DEVICE_CMD_GETENERGY,
+            DEVICE_CMD_GETCHILDDEVICELIST);
+    public static final Set<String> DEVICE_CMDLIST_SET = Set.of(DEVICE_CMD_SETINFO, DEVICE_CMD_SET_DYNAIMCLIGHT_FX,
+            DEVICE_CMD_CONTROL_CHILD);
+
+    public static final int LOGIN_RETRIES = 1;
+}
index af792871d5837bb57f3f0981a74f758f8dc18e60..5c7e411fa1719324718568d40946efc01517becc 100644 (file)
@@ -22,11 +22,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 @NonNullByDefault
 public enum TapoErrorCode {
     NO_ERROR(0),
+    // List of API-Errorcodes from device
     ERR_UNKNOWN(-1, TapoErrorType.UNKNOWN),
-    ERR_API_SESSION_TIMEOUT(9999, TapoErrorType.COMMUNICATION_RETRY),
+    ERR_API_UNKNOWN_COM_ERROR(9999, TapoErrorType.COMMUNICATION_ERROR),
     ERR_API_NULL_TRANSPORT(1000),
     ERR_API_REQUEST(1002),
-    ERR_API_HAND_SHAKE_FAILED(1100, TapoErrorType.COMMUNICATION_RETRY),
+    ERR_API_PROTOCOL(1003, TapoErrorType.CONFIGURATION_ERROR),
+    ERR_API_HAND_SHAKE_FAILED(1100, TapoErrorType.COMMUNICATION_ERROR),
     ERR_API_LOGIN_FAILED(1111),
     ERR_API_HTTP_TRANSPORT_FAILED(1112),
     ERR_API_MULTI_REQUEST_FAILED(1200),
@@ -75,16 +77,27 @@ public enum TapoErrorCode {
     ERR_CLOUD_TOKEN_EXPIRED(-20651),
 
     // List of Binding-ErrorCodes
+    ERR_BINDING_NOT_IMPLEMENTED(9000),
     ERR_BINDING_HTTP_RESPONSE(9001, TapoErrorType.COMMUNICATION_ERROR),
     ERR_BINDING_COOKIE(9002, TapoErrorType.COMMUNICATION_ERROR),
     ERR_BINDING_CREDENTIALS(9003, TapoErrorType.CONFIGURATION_ERROR),
+    ERR_BINDING_LOGIN(9004, TapoErrorType.CONFIGURATION_ERROR),
     ERR_BINDING_DEVICE_OFFLINE(9009, TapoErrorType.COMMUNICATION_ERROR),
     ERR_BINDING_CONNECT_TIMEOUT(9010, TapoErrorType.COMMUNICATION_ERROR),
+    ERR_BINDING_SEND_REQUEST(9011, TapoErrorType.COMMUNICATION_ERROR),
+    ERR_BINDING_FX_NOT_FOUND(9020, TapoErrorType.CONFIGURATION_ERROR),
+
+    // List of Data-Error
+    ERR_DATA_ENCRYPTING(9500, TapoErrorType.COMMUNICATION_ERROR),
+    ERR_DATA_DECRYPTING(9501, TapoErrorType.COMMUNICATION_ERROR),
+    ERR_DATA_FORMAT(9505, TapoErrorType.COMMUNICATION_ERROR),
+    ERR_DATA_TRANSORMATION(9506),
 
     // List of Binding-Config-ErrorCodes
     ERR_CONFIG_IP(10001, TapoErrorType.CONFIGURATION_ERROR), // ip not set
     ERR_CONFIG_CREDENTIALS(10002, TapoErrorType.CONFIGURATION_ERROR), // credentials not set
-    ERR_CONFIG_NO_BRIDGE(10003, TapoErrorType.CONFIGURATION_ERROR); // no bridge configured
+    ERR_CONFIG_NO_BRIDGE(10003, TapoErrorType.CONFIGURATION_ERROR), // no bridge configured
+    ERR_CONFIG_PROTOCOL(10004, TapoErrorType.CONFIGURATION_ERROR); // unknown protocol
 
     private Integer code;
     private TapoErrorType errorType;
index dbc1dac0470a387cc16f99ea6d7835228bb761fe..0286783a64c849651506a7d9d169e5f7b5b56a1a 100644 (file)
@@ -23,7 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.thing.ThingTypeUID;
 
 /**
- * The {@link TapoBindingSettings} class defines common constants, which are
+ * The {@link TapoThingConstants} class defines thing constants, which are
  * used across the whole binding.
  *
  * @author Christian Wild - Initial contribution
@@ -34,6 +34,7 @@ public class TapoThingConstants {
 
     /*** LIST OF SUPPORTED DEVICE NAMES ***/
     public static final String DEVICE_BRIDGE = "bridge";
+    public static final String DEVICE_H100 = "H100";
     public static final String DEVICE_P100 = "P100";
     public static final String DEVICE_P105 = "P105";
     public static final String DEVICE_P110 = "P110";
@@ -46,18 +47,26 @@ public class TapoThingConstants {
     public static final String DEVICE_L900 = "L900";
     public static final String DEVICE_L920 = "L920";
     public static final String DEVICE_L930 = "L930";
+    public static final String DEVICE_T110 = "T110";
+    public static final String DEVICE_T310 = "T310";
+    public static final String DEVICE_T315 = "T315";
     public static final String DEVICE_UNIVERSAL = "Test_Device";
 
     /*** LIST OF SUPPORTED DEVICE DESCRIPTIONS ***/
     public static final String DEVICE_DESCRIPTION_BRIDGE = "TapoControl Cloud-Login";
-    public static final String DEVICE_DESCRIPTION_SMART_PLUG = "SmartPlug";
-    public static final String DEVICE_DESCRIPTION_POWER_STRIP = "PowerStrip";
+    public static final String DEVICE_DESCRIPTION_HUB = "SmartHub";
+    public static final String DEVICE_DESCRIPTION_SOCKET = "SmartPlug";
+    public static final String DEVICE_DESCRIPTION_SOCKET_STRIP = "PowerStrip";
     public static final String DEVICE_DESCRIPTION_WHITE_BULB = "White-Light-Bulb";
     public static final String DEVICE_DESCRIPTION_COLOR_BULB = "Color-Light-Bulb";
     public static final String DEVICE_DESCRIPTION_LIGHTSTRIP = "LightStrip";
+    public static final String DEVICE_DESCRIPTION_SMART_CONTACT = "Smart-Contact-Sensor";
+    public static final String DEVICE_DESCRIPTION_MOTION_SENSOR = "Motion-Sensor";
+    public static final String DEVICE_DESCRIPTION_TEMP_SENSOR = "Temperature-Sensor";
 
     /*** LIST OF SUPPORTED THING UIDS ***/
     public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_BRIDGE);
+    public static final ThingTypeUID H100_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_H100);
     public static final ThingTypeUID P100_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P100);
     public static final ThingTypeUID P105_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P105);
     public static final ThingTypeUID P110_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P110);
@@ -72,92 +81,45 @@ public class TapoThingConstants {
     public static final ThingTypeUID L930_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L930);
     public static final ThingTypeUID UNIVERSAL_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_UNIVERSAL);
 
+    /*** LIST OF SUPPORTED HUB CHILD THING UIDS ***/
+    public static final ThingTypeUID T110_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_T110);
+    public static final ThingTypeUID T310_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_T310);
+    public static final ThingTypeUID T315_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_T315);
+
     /*** SET OF SUPPORTED UIDS ***/
     public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_UIDS = Set.of(BRIDGE_THING_TYPE);
-    public static final Set<ThingTypeUID> SUPPORTED_SMART_PLUG_UIDS = Set.of(P100_THING_TYPE, P105_THING_TYPE,
-            P110_THING_TYPE, P115_THING_TYPE, P300_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_HUB_UIDS = Set.of(H100_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_SOCKET_UIDS = Set.of(P100_THING_TYPE, P105_THING_TYPE,
+            P110_THING_TYPE, P115_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_SOCKET_STRIP_UIDS = Set.of(P300_THING_TYPE);
     public static final Set<ThingTypeUID> SUPPORTED_WHITE_BULB_UIDS = Set.of(L510_THING_TYPE, L610_THING_TYPE);
     public static final Set<ThingTypeUID> SUPPORTED_COLOR_BULB_UIDS = Set.of(L530_THING_TYPE, L630_THING_TYPE);
     public static final Set<ThingTypeUID> SUPPORTED_LIGHT_STRIP_UIDS = Set.of(L900_THING_TYPE, L920_THING_TYPE,
             L930_THING_TYPE);
-    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
-            .unmodifiableSet(Stream
-                    .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_SMART_PLUG_UIDS, SUPPORTED_WHITE_BULB_UIDS,
-                            SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS)
-                    .flatMap(Set::stream).collect(Collectors.toSet()));
+    public static final Set<ThingTypeUID> SUPPORTED_HUB_CHILD_TYPES_UIDS = Set.of(T110_THING_TYPE, T310_THING_TYPE,
+            T315_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_SMART_CONTACTS = Set.of(T110_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_MOTION_SENSORS = Set.of();
+    public static final Set<ThingTypeUID> SUPPORTED_WHEATHER_SENSORS = Set.of(T310_THING_TYPE, T315_THING_TYPE);
+
+    /*** SET OF ALL SUPPORTED THINGS ***/
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
+            .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_HUB_UIDS, SUPPORTED_SOCKET_UIDS, SUPPORTED_SOCKET_STRIP_UIDS,
+                    SUPPORTED_WHITE_BULB_UIDS, SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS,
+                    SUPPORTED_SMART_CONTACTS, SUPPORTED_MOTION_SENSORS, SUPPORTED_WHEATHER_SENSORS)
+            .flatMap(Set::stream).collect(Collectors.toSet()));
+
     /*** THINGS WITH ENERGY DATA ***/
     public static final Set<ThingTypeUID> SUPPORTED_ENERGY_DATA_UIDS = Set.of(P110_THING_TYPE, P115_THING_TYPE);
 
-    /*** THINGS WITH CHILDS DATA ***/
-    public static final Set<ThingTypeUID> SUPPORTED_CHILDS_DATA_UIDS = Set.of(P300_THING_TYPE);
-
     /*** THINGS WITH CHANNEL GROUPS ***/
-    public static final Set<ThingTypeUID> CHANNEL_GROUP_THING_SET = Collections
-            .unmodifiableSet(Stream
-                    .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_SMART_PLUG_UIDS, SUPPORTED_WHITE_BULB_UIDS,
-                            SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS)
-                    .flatMap(Set::stream).collect(Collectors.toSet()));
-
-    /*** DEVICE JSON STRINGS (CLOUD) ***/
-    public static final String CLOUD_JSON_KEY_ALIAS = "alias";
-    public static final String CLOUD_JSON_KEY_FW = "fwVer";
-    public static final String CLOUD_JSON_KEY_HW = "deviceHwVer";
-    public static final String CLOUD_JSON_KEY_ID = "deviceId";
-    public static final String CLOUD_JSON_KEY_MAC = "deviceMac";
-    public static final String CLOUD_JSON_KEY_MODEL = "deviceName"; // use name cause modell returns different values
-    public static final String CLOUD_JSON_KEY_NAME = "deviceName";
-    public static final String CLOUD_JSON_KEY_REGION = "deviceRegion";
-    public static final String CLOUD_JSON_KEY_SERVER_URL = "appServerUrl";
-    public static final String CLOUD_JSON_KEY_TYPE = "deviceType";
-
-    /*** DEVICE JSON STRINGS (DEVICE) ***/
-    public static final String JSON_KEY_BRIGHTNESS = "brightness";
-    public static final String JSON_KEY_COLORTEMP = "color_temp";
-    public static final String JSON_KEY_FW = "fw_ver";
-    public static final String JSON_KEY_HUE = "hue";
-    public static final String JSON_KEY_HW_VER = "hw_ver";
-    public static final String JSON_KEY_ID = "device_id";
-    public static final String JSON_KEY_IP = "ip";
-    public static final String JSON_KEY_MAC = "mac";
-    public static final String JSON_KEY_MODEL = "model";
-    public static final String JSON_KEY_NICKNAME = "nickname";
-    public static final String JSON_KEY_ON = "device_on";
-    public static final String JSON_KEY_ONTIME = "on_time";
-    public static final String JSON_KEY_OVERHEAT = "overheated";
-    public static final String JSON_KEY_REGION = "region";
-    public static final String JSON_KEY_SATURATION = "saturation";
-    public static final String JSON_KEY_SIGNAL_LEVEL = "signal_level";
-    public static final String JSON_KEY_RSSI = "rssi";
-    public static final String JSON_KEY_TYPE = "type";
-    public static final String JSON_KEY_USAGE_7 = "time_usage_past7";
-    public static final String JSON_KEY_USAGE_30 = "time_usage_past30";
-    public static final String JSON_KEY_USAGE_TODAY = "time_usage_today";
-    public static final String DEVICE_REPRESENTATION_PROPERTY = "macAddress";
-    // lightning effects
-    public static final String JSON_KEY_LIGHTNING_EFFECT = "lighting_effect";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS = "brightness";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_COLORTEMPRANGE = "color_temp_range";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_CUSTOM = "custom";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_OFF = "off";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_DISPLAYCOLORS = "displayColors";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_ENABLE = "enable";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_ID = "id";
-    public static final String JSON_KEY_LIGHTNING_EFFECT_NAME = "name";
-    public static final String JSON_KEY_LIGHTNING_DYNAMIC_ENABLE = "dynamic_light_effect_enable";
-    public static final String JSON_KEY_LIGHTNING_DYNAMIC_ID = "dynamic_light_effect_id";
-
-    // energy monitoring
-    public static final String JSON_KEY_ENERGY_POWER = "current_power";
-    public static final String JSON_KEY_ENERGY_RUNTIME_TODAY = "today_runtime";
-    public static final String JSON_KEY_ENERGY_RUNTIME_MONTH = "month_runtime";
-    public static final String JSON_KEY_ENERGY_ENERGY_TODAY = "today_energy";
-    public static final String JSON_KEY_ENERGY_ENERGY_MONTH = "month_energy";
-    public static final String JSON_KEY_ENERGY_PAST24H = "past24h";
-    public static final String JSON_KEY_ENERGY_PAST7D = "past7d";
-    public static final String JSON_KEY_ENERGY_PAST30D = "past30d";
-    public static final String JSON_KEY_ENERGY_PAST1Y = "past1y";
-    // childs management
-    public static final String JSON_KEY_CHILD_START_INDEX = "start_index";
+    public static final Set<ThingTypeUID> CHANNEL_GROUP_THING_SET = Collections.unmodifiableSet(Stream
+            .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_HUB_UIDS, SUPPORTED_SOCKET_UIDS, SUPPORTED_SOCKET_STRIP_UIDS,
+                    SUPPORTED_WHITE_BULB_UIDS, SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS,
+                    SUPPORTED_SMART_CONTACTS, SUPPORTED_MOTION_SENSORS, SUPPORTED_WHEATHER_SENSORS)
+            .flatMap(Set::stream).collect(Collectors.toSet()));
+
+    public static final String CHILD_REPRESENTATION_PROPERTY = "serialNumber";
 
     /*** DEVICE SETTINGS ***/
     public static final Integer BULB_MIN_COLORTEMP = 2500;
@@ -169,18 +131,32 @@ public class TapoThingConstants {
     public static final String CHANNEL_BRIGHTNESS = "brightness";
     public static final String CHANNEL_COLOR = "color";
     public static final String CHANNEL_COLOR_TEMP = "colorTemperature";
+    public static final String CHANNEL_MODE = "mode";
     public static final String CHANNEL_OUTPUT = "output";
     public static final String CHANNEL_SWITCH = "switch";
     // channel group device
     public static final String CHANNEL_GROUP_DEVICE = "device";
+    public static final String CHANNEL_BATTERY_LOW = "batteryLow";
     public static final String CHANNEL_ONTIME = "onTime";
     public static final String CHANNEL_OVERHEAT = "overheated";
+    public static final String CHANNEL_SIGNAL_STRENGTH = "signalStrength";
     public static final String CHANNEL_WIFI_STRENGTH = "wifiSignal";
+    // channel group alarm
+    public static final String CHANNEL_GROUP_ALARM = "alarm";
+    public static final String CHANNEL_ALARM_ACTIVE = "alarmActive";
+    public static final String CHANNEL_ALARM_SOURCE = "alarmSource";
+    // channel group sensor
+    public static final String CHANNEL_GROUP_SENSOR = "sensor";
+    public static final String CHANNEL_IS_OPEN = "isOpen";
+    public static final String CHANNEL_TEMPERATURE = "currentTemp";
+    public static final String CHANNEL_HUMIDITY = "currentHumidity";
     // channel group energy monitor
     public static final String CHANNEL_GROUP_ENERGY = "energy";
     public static final String CHANNEL_NRG_POWER = "actualPower";
     public static final String CHANNEL_NRG_USAGE_TODAY = "todayEnergyUsage";
     public static final String CHANNEL_NRG_RUNTIME_TODAY = "todayRuntime";
+    public static final String CHANNEL_NRG_USAGE_MONTH = "monthEnergyUsage";
+    public static final String CHANNEL_NRG_RUNTIME_MONTH = "monthRuntime";
     // channel group effect
     public static final String CHANNEL_GROUP_EFFECTS = "effects";
     public static final String CHANNEL_FX_BRIGHTNESS = "fxBrightness";
@@ -191,4 +167,13 @@ public class TapoThingConstants {
     public static final String PROPERTY_FAMILY = "deviceFamily";
     public static final String PROPERTY_LOCATION = "location";
     public static final String PROPERTY_WIFI_LEVEL = "signal-strength";
+
+    /*** EVENT LISTS ***/
+    // hub child events
+    public static final String EVENT_BATTERY_LOW = "batteryIsLow";
+    public static final String EVENT_CONTACT_OPENED = "contactOpened";
+    public static final String EVENT_CONTACT_CLOSED = "contactClosed";
+    public static final String EVENT_STATE_BATTERY_LOW = "batteryLow";
+    public static final String EVENT_STATE_OPENED = "open";
+    public static final String EVENT_STATE_CLOSED = "closed";
 }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java
deleted file mode 100644 (file)
index 5624097..0000000
+++ /dev/null
@@ -1,305 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import java.util.Collection;
-import java.util.Set;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.tapocontrol.internal.TapoDiscoveryService;
-import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
-import org.openhab.core.thing.Bridge;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.ThingUID;
-import org.openhab.core.thing.binding.BaseBridgeHandler;
-import org.openhab.core.thing.binding.ThingHandlerService;
-import org.openhab.core.types.Command;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-
-/**
- * The {@link TapoBridgeHandler} is responsible for handling commands, which are
- * sent to one of the channels with a bridge.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoBridgeHandler extends BaseBridgeHandler {
-    private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
-    private final TapoErrorHandler bridgeError = new TapoErrorHandler();
-    private TapoBridgeConfiguration config = new TapoBridgeConfiguration();
-    private final HttpClient httpClient;
-    private @Nullable ScheduledFuture<?> startupJob;
-    private @Nullable ScheduledFuture<?> pollingJob;
-    private @Nullable ScheduledFuture<?> discoveryJob;
-    private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
-    private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
-    private TapoCredentials credentials;
-
-    private String uid;
-
-    public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
-        super(bridge);
-        Thing thing = getThing();
-        this.cloudConnector = new TapoCloudConnector(this, httpClient);
-        this.credentials = new TapoCredentials();
-        this.uid = thing.getUID().toString();
-        this.httpClient = httpClient;
-    }
-
-    /***********************************
-     *
-     * BRIDGE INITIALIZATION
-     *
-     ************************************/
-    @Override
-    /**
-     * INIT BRIDGE
-     * set credentials and login cloud
-     */
-    public void initialize() {
-        this.config = getConfigAs(TapoBridgeConfiguration.class);
-        this.credentials = new TapoCredentials(config.username, config.password);
-        activateBridge();
-    }
-
-    /**
-     * ACTIVATE BRIDGE
-     */
-    private void activateBridge() {
-        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
-        updateStatus(ThingStatus.UNKNOWN);
-
-        // background initialization (delay it a little bit):
-        this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
-    }
-
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
-    }
-
-    @Override
-    public void dispose() {
-        stopScheduler(this.startupJob);
-        stopScheduler(this.pollingJob);
-        stopScheduler(this.discoveryJob);
-        super.dispose();
-    }
-
-    /**
-     * ACTIVATE DISCOVERY SERVICE
-     */
-    @Override
-    public Collection<Class<? extends ThingHandlerService>> getServices() {
-        return Set.of(TapoDiscoveryService.class);
-    }
-
-    /**
-     * Set DiscoveryService
-     * 
-     * @param discoveryService
-     */
-    public void setDiscoveryService(TapoDiscoveryService discoveryService) {
-        this.discoveryService = discoveryService;
-    }
-
-    /***********************************
-     *
-     * SCHEDULER
-     *
-     ************************************/
-
-    /**
-     * delayed OneTime StartupJob
-     */
-    private void delayedStartUp() {
-        loginCloud();
-        startCloudScheduler();
-        startDiscoveryScheduler();
-    }
-
-    /**
-     * Start CloudLogin Scheduler
-     */
-    protected void startCloudScheduler() {
-        int pollingInterval = config.reconnectInterval;
-        TimeUnit timeUnit = TimeUnit.MINUTES;
-        if (pollingInterval > 0) {
-            logger.debug("{} starting cloudScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
-
-            this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
-                    timeUnit);
-        } else {
-            logger.debug("({}) cloudScheduler disabled with config '0'", uid);
-            stopScheduler(this.pollingJob);
-        }
-    }
-
-    /**
-     * Start DeviceDiscovery Scheduler
-     */
-    protected void startDiscoveryScheduler() {
-        int pollingInterval = config.discoveryInterval;
-        TimeUnit timeUnit = TimeUnit.MINUTES;
-        if (config.cloudDiscovery && pollingInterval > 0) {
-            logger.debug("{} starting discoveryScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
-
-            this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval, timeUnit);
-        } else {
-            logger.debug("({}) discoveryScheduler disabled with config '0'", uid);
-            stopScheduler(this.discoveryJob);
-        }
-    }
-
-    /**
-     * Stop scheduler
-     * 
-     * @param scheduler {@code ScheduledFeature<?>} which schould be stopped
-     */
-    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
-        if (scheduler != null) {
-            scheduler.cancel(true);
-            scheduler = null;
-        }
-    }
-
-    /***********************************
-     *
-     * ERROR HANDLER
-     *
-     ************************************/
-    /**
-     * return device Error
-     * 
-     * @return
-     */
-    public TapoErrorHandler getError() {
-        return this.bridgeError;
-    }
-
-    /**
-     * set device error
-     * 
-     * @param tapoError TapoErrorHandler-Object
-     */
-    public void setError(TapoErrorHandler tapoError) {
-        this.bridgeError.set(tapoError);
-    }
-
-    /***********************************
-     *
-     * BRIDGE COMMUNICATIONS
-     *
-     ************************************/
-
-    /**
-     * Login to Cloud
-     * 
-     * @return
-     */
-    public boolean loginCloud() {
-        bridgeError.reset(); // reset ErrorHandler
-        if (!config.username.isBlank() && !config.password.isBlank()) {
-            logger.debug("{} login with user {}", this.uid, config.username);
-            if (cloudConnector.login(config.username, config.password)) {
-                updateStatus(ThingStatus.ONLINE);
-                return true;
-            } else {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
-            }
-        } else {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set");
-        }
-        return false;
-    }
-
-    /***********************************
-     *
-     * DEVICE DISCOVERY
-     *
-     ************************************/
-
-    /**
-     * START DEVICE DISCOVERY
-     */
-    public void discoverDevices() {
-        this.discoveryService.startScan();
-    }
-
-    /**
-     * GET DEVICELIST CONNECTED TO BRIDGE
-     * 
-     * @return devicelist
-     */
-    public JsonArray getDeviceList() {
-        JsonArray deviceList = new JsonArray();
-        if (config.cloudDiscovery) {
-            logger.trace("{} discover devicelist from cloud", this.uid);
-            deviceList = getDeviceListCloud();
-        } else {
-            logger.info("{} Discovery disabled in bridge settings ", this.uid);
-        }
-        return deviceList;
-    }
-
-    /**
-     * GET DEVICELIST FROM CLOUD
-     * returns all devices stored in cloud
-     * 
-     * @return deviceList from cloud
-     */
-    private JsonArray getDeviceListCloud() {
-        logger.trace("{} getDeviceList from cloud", this.uid);
-        bridgeError.reset(); // reset ErrorHandler
-        JsonArray deviceList = new JsonArray();
-        if (loginCloud()) {
-            deviceList = this.cloudConnector.getDeviceList();
-        }
-        return deviceList;
-    }
-
-    /***********************************
-     *
-     * BRIDGE GETTERS
-     *
-     ************************************/
-
-    public TapoCredentials getCredentials() {
-        return this.credentials;
-    }
-
-    public HttpClient getHttpClient() {
-        return this.httpClient;
-    }
-
-    public ThingUID getUID() {
-        return getThing().getUID();
-    }
-
-    public TapoBridgeConfiguration getBridgeConfig() {
-        return this.config;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java
deleted file mode 100644 (file)
index 2158f78..0000000
+++ /dev/null
@@ -1,529 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector;
-import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoChildData;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceConfiguration;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoEnergyData;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.Bridge;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.thing.binding.BridgeHandler;
-import org.openhab.core.types.State;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Abstract class as base for TAPO-Device device implementations.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public abstract class TapoDevice extends BaseThingHandler {
-    private final Logger logger = LoggerFactory.getLogger(TapoDevice.class);
-    protected final TapoErrorHandler deviceError = new TapoErrorHandler();
-    protected final String uid;
-    protected TapoDeviceConfiguration config = new TapoDeviceConfiguration();
-    protected TapoDeviceInfo deviceInfo;
-    protected @Nullable ScheduledFuture<?> startupJob;
-    protected @Nullable ScheduledFuture<?> pollingJob;
-    protected @NonNullByDefault({}) TapoDeviceConnector connector;
-    protected @NonNullByDefault({}) TapoBridgeHandler bridge;
-
-    /**
-     * Constructor
-     *
-     * @param thing Thing object representing device
-     */
-    protected TapoDevice(Thing thing) {
-        super(thing);
-        this.deviceInfo = new TapoDeviceInfo();
-        this.uid = getThing().getUID().getAsString();
-    }
-
-    /***********************************
-     *
-     * INIT AND SETTINGS
-     *
-     ************************************/
-
-    /**
-     * INITIALIZE DEVICE
-     */
-    @Override
-    public void initialize() {
-        try {
-            this.config = getConfigAs(TapoDeviceConfiguration.class);
-            Bridge bridgeThing = getBridge();
-            if (bridgeThing != null) {
-                BridgeHandler bridgeHandler = bridgeThing.getHandler();
-                if (bridgeHandler != null) {
-                    this.bridge = (TapoBridgeHandler) bridgeHandler;
-                    this.connector = new TapoDeviceConnector(this, bridge);
-                }
-            }
-        } catch (Exception e) {
-            logger.debug("({}) configuration error : {}", uid, e.getMessage());
-        }
-        TapoErrorHandler configError = checkSettings();
-        if (!configError.hasError()) {
-            activateDevice();
-        } else {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
-        }
-    }
-
-    /**
-     * DISPOSE
-     */
-    @Override
-    public void dispose() {
-        try {
-            stopScheduler(this.startupJob);
-            stopScheduler(this.pollingJob);
-            connector.logout();
-        } catch (Exception e) {
-            // handle exception
-        }
-        super.dispose();
-    }
-
-    /**
-     * ACTIVATE DEVICE
-     */
-    private void activateDevice() {
-        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
-        updateStatus(ThingStatus.UNKNOWN);
-
-        // background initialization (delay it a little bit):
-        this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
-    }
-
-    /**
-     * CHECK SETTINGS
-     *
-     * @return TapoErrorHandler with configuration-errors
-     */
-    protected TapoErrorHandler checkSettings() {
-        TapoErrorHandler configErr = new TapoErrorHandler();
-
-        /* check bridge */
-        if (!(bridge instanceof TapoBridgeHandler)) {
-            configErr.raiseError(ERR_CONFIG_NO_BRIDGE);
-            return configErr;
-        }
-        /* check ip-address */
-        if (!config.ipAddress.matches(IPV4_REGEX)) {
-            configErr.raiseError(ERR_CONFIG_IP);
-            return configErr;
-        }
-        /* check credentials */
-        if (!bridge.getCredentials().areSet()) {
-            configErr.raiseError(ERR_CONFIG_CREDENTIALS);
-            return configErr;
-        }
-        return configErr;
-    }
-
-    /**
-     * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
-     *
-     * @throws IOException if an error code was set in the response object
-     */
-    protected void checkErrors() throws IOException {
-        final Integer errorCode = deviceError.getCode();
-
-        if (errorCode != 0) {
-            throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
-        }
-    }
-
-    /***********************************
-     *
-     * SCHEDULER
-     *
-     ************************************/
-    /**
-     * delayed OneTime StartupJob
-     */
-    private void delayedStartUp() {
-        connect();
-        startPollingScheduler();
-    }
-
-    /**
-     * Start scheduler
-     */
-    protected void startPollingScheduler() {
-        int pollingInterval = this.config.pollingInterval;
-        TimeUnit timeUnit = TimeUnit.SECONDS;
-
-        if (pollingInterval > 0) {
-            if (pollingInterval < POLLING_MIN_INTERVAL_S) {
-                pollingInterval = POLLING_MIN_INTERVAL_S;
-            }
-            logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
-            this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
-                    pollingInterval, timeUnit);
-        } else {
-            logger.debug("({}) scheduler disabled with config '0'", uid);
-            stopScheduler(this.pollingJob);
-        }
-    }
-
-    /**
-     * Stop scheduler
-     *
-     * @param scheduler {@code ScheduledFeature<?>} which schould be stopped
-     */
-    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
-        if (scheduler != null) {
-            scheduler.cancel(true);
-            scheduler = null;
-        }
-    }
-
-    /**
-     * Scheduler Action
-     */
-    protected void pollingSchedulerAction() {
-        logger.trace("({}) schedulerAction", uid);
-        queryDeviceInfo();
-    }
-
-    /***********************************
-     *
-     * ERROR HANDLER
-     *
-     ************************************/
-    /**
-     * return device Error
-     *
-     * @return
-     */
-    public TapoErrorHandler getErrorHandler() {
-        return this.deviceError;
-    }
-
-    public TapoErrorCode getError() {
-        return this.deviceError.getError();
-    }
-
-    /**
-     * set device error
-     *
-     * @param tapoError TapoErrorHandler-Object
-     */
-    public void setError(TapoErrorHandler tapoError) {
-        this.deviceError.set(tapoError);
-        handleConnectionState();
-    }
-
-    /***********************************
-     *
-     * THING
-     *
-     ************************************/
-
-    /***
-     * Check if ThingType is model
-     *
-     * @param model
-     * @return
-     */
-    protected Boolean isThingModel(String model) {
-        try {
-            ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model);
-            ThingTypeUID expectedType = getThing().getThingTypeUID();
-            return expectedType.equals(foundType);
-        } catch (Exception e) {
-            logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
-            return false;
-        }
-    }
-
-    /**
-     * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
-     * Compare MAC-Adress
-     *
-     * @param deviceInfo
-     * @return true if is the expected device
-     */
-    protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
-        try {
-            String expectedThingUID = getThing().getProperties().get(DEVICE_REPRESENTATION_PROPERTY);
-            String foundThingUID = deviceInfo.getRepresentationProperty();
-            String foundModel = deviceInfo.getModel();
-            if (expectedThingUID == null || expectedThingUID.isBlank()) {
-                return isThingModel(foundModel);
-            }
-            /* sometimes received mac was with and sometimes without "-" from device */
-            expectedThingUID = unformatMac(expectedThingUID);
-            foundThingUID = unformatMac(foundThingUID);
-            return expectedThingUID.equals(foundThingUID);
-        } catch (Exception e) {
-            logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
-            return false;
-        }
-    }
-
-    /**
-     * Return ThingUID
-     */
-    public ThingUID getThingUID() {
-        return getThing().getUID();
-    }
-
-    /***********************************
-     *
-     * DEVICE PROPERTIES
-     *
-     ************************************/
-
-    /**
-     * query device Properties
-     */
-    public void queryDeviceInfo() {
-        queryDeviceInfo(false);
-    }
-
-    /**
-     * query device Properties
-     *
-     * @param ignoreGap ignore gap to last query. query anyway (force)
-     */
-    public void queryDeviceInfo(boolean ignoreGap) {
-        deviceError.reset();
-        if (connector.loggedIn()) {
-            connector.queryInfo(ignoreGap);
-            // query energy usage
-            if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
-                connector.getEnergyUsage();
-            }
-            // query childs data
-            if (SUPPORTED_CHILDS_DATA_UIDS.contains(getThing().getThingTypeUID())) {
-                connector.queryChildDevices();
-            }
-        } else {
-            logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
-            connect();
-        }
-    }
-
-    /**
-     * SET DEVICE INFOs to device
-     *
-     * @param deviceInfo
-     */
-    public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
-        this.deviceInfo = deviceInfo;
-        if (isExpectedThing(deviceInfo)) {
-            devicePropertiesChanged(deviceInfo);
-            handleConnectionState();
-        } else {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
-                    "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
-                            + "'. Check IP-Address");
-        }
-    }
-
-    /**
-     * Set Device EnergyData to device
-     *
-     * @param energyData
-     */
-    public void setEnergyData(TapoEnergyData energyData) {
-        publishState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_POWER),
-                getPowerType(energyData.getCurrentPower(), Units.WATT));
-        publishState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_USAGE_TODAY),
-                getEnergyType(energyData.getTodayEnergy(), Units.WATT_HOUR));
-        publishState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_RUNTIME_TODAY),
-                getTimeType(energyData.getTodayRuntime(), Units.MINUTE));
-    }
-
-    /**
-     * Set Device Child data to device
-     *
-     * @param hostData
-     */
-    public void setChildData(TapoChildData hostData) {
-        hostData.getChildDeviceList().forEach(child -> {
-            publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT + Integer.toString(child.getPosition())),
-                    getOnOffType(child.getDeviceOn()));
-        });
-    }
-
-    /**
-     * Handle full responsebody received from connector
-     *
-     * @param responseBody
-     */
-    public void responsePasstrough(String responseBody) {
-    }
-
-    /**
-     * UPDATE PROPERTIES
-     *
-     * If only one property must be changed, there is also a convenient method
-     * updateProperty(String name, String value).
-     *
-     * @param deviceInfo
-     */
-    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
-        /* device properties */
-        Map<String, String> properties = editProperties();
-        properties.put(Thing.PROPERTY_MAC_ADDRESS, deviceInfo.getMAC());
-        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceInfo.getFirmwareVersion());
-        properties.put(Thing.PROPERTY_HARDWARE_VERSION, deviceInfo.getHardwareVersion());
-        properties.put(Thing.PROPERTY_MODEL_ID, deviceInfo.getModel());
-        properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceInfo.getSerial());
-        updateProperties(properties);
-    }
-
-    /**
-     * update channel state
-     *
-     * @param channelID
-     * @param value
-     */
-    public void publishState(String channelID, State value) {
-        updateState(channelID, value);
-    }
-
-    /***********************************
-     *
-     * CONNECTION
-     *
-     ************************************/
-
-    /**
-     * Connect (login) to device
-     *
-     */
-    public Boolean connect() {
-        deviceError.reset();
-        Boolean loginSuccess = false;
-
-        try {
-            loginSuccess = connector.login();
-            if (loginSuccess) {
-                queryDeviceInfo(true);
-            } else {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
-            }
-        } catch (Exception e) {
-            updateStatus(ThingStatus.UNKNOWN);
-        }
-        return loginSuccess;
-    }
-
-    /**
-     * disconnect device
-     */
-    public void disconnect() {
-        connector.logout();
-    }
-
-    /**
-     * handle device state by connector error
-     */
-    public void handleConnectionState() {
-        ThingStatus deviceState = getThing().getStatus();
-        TapoErrorCode errorCode = deviceError.getError();
-
-        if (errorCode == TapoErrorCode.NO_ERROR) {
-            if (deviceState != ThingStatus.ONLINE) {
-                updateStatus(ThingStatus.ONLINE);
-            }
-        } else {
-            switch (errorCode.getType()) {
-                case COMMUNICATION_RETRY:
-                    connect();
-                    break;
-                case COMMUNICATION_ERROR:
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
-                    disconnect();
-                    break;
-                case CONFIGURATION_ERROR:
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
-                    break;
-                default:
-                    updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
-            }
-        }
-    }
-
-    /**
-     * Return IP-Address of device
-     */
-    public String getIpAddress() {
-        return this.config.ipAddress;
-    }
-
-    /***********************************
-     *
-     * CHANNELS
-     *
-     ************************************/
-    /**
-     * Get ChannelID including group
-     *
-     * @param group String channel-group
-     * @param channel String channel-name
-     * @return String channelID
-     */
-    protected String getChannelID(String group, String channel) {
-        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
-        if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
-            return group + "#" + channel;
-        }
-        return channel;
-    }
-
-    /**
-     * Get Channel from ChannelID
-     *
-     * @param channelID String channelID
-     * @return String channel-name
-     */
-    protected String getChannelFromID(ChannelUID channelID) {
-        String channel = channelID.getIdWithoutGroup();
-        channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_EFFECTS + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_ENERGY + "#", "");
-        return channel;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java
deleted file mode 100644 (file)
index d38df6c..0000000
+++ /dev/null
@@ -1,225 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoLightEffect;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonObject;
-
-/**
- * TAPO Smart-Plug-Device.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoLightStrip extends TapoDevice {
-    private final Logger logger = LoggerFactory.getLogger(TapoLightStrip.class);
-
-    /**
-     * Constructor
-     * 
-     * @param thing Thing object representing device
-     */
-    public TapoLightStrip(Thing thing) {
-        super(thing);
-    }
-
-    /**
-     * handle command sent to device
-     * 
-     * @param channelUID channelUID command is sent to
-     * @param command command to be sent
-     */
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        Boolean refreshInfo = false;
-
-        String channel = channelUID.getIdWithoutGroup();
-        String group = channelUID.getGroupId();
-        if (command instanceof RefreshType) {
-            refreshInfo = true;
-        } else if (group == CHANNEL_GROUP_EFFECTS) {
-            setLightEffect(channel, command);
-            refreshInfo = true;
-        } else {
-            switch (channel) {
-                case CHANNEL_OUTPUT:
-                    connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
-                    refreshInfo = true;
-                    break;
-                case CHANNEL_BRIGHTNESS:
-                    if (command instanceof PercentType percentCommand) {
-                        Float percent = percentCommand.floatValue();
-                        setBrightness(percent.intValue()); // 0..100% = 0..100
-                        refreshInfo = true;
-                    } else if (command instanceof DecimalType decimalCommand) {
-                        setBrightness(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR_TEMP:
-                    if (command instanceof DecimalType decimalCommand) {
-                        setColorTemp(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR:
-                    if (command instanceof HSBType hsbCommand) {
-                        setColor(hsbCommand);
-                        refreshInfo = true;
-                    }
-                    break;
-                default:
-                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
-                            channelUID.getId());
-            }
-        }
-
-        /* refreshInfo */
-        if (refreshInfo) {
-            queryDeviceInfo(true);
-        }
-    }
-
-    /**
-     * SET BRIGHTNESS
-     * 
-     * @param newBrightness percentage 0-100 of new brightness
-     */
-    protected void setBrightness(Integer newBrightness) {
-        /* switch off if 0 */
-        if (newBrightness == 0) {
-            connector.sendDeviceCommand(JSON_KEY_ON, false);
-        } else {
-            HashMap<String, Object> newState = new HashMap<>();
-            newState.put(JSON_KEY_ON, true);
-            newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
-            connector.sendDeviceCommands(newState);
-        }
-    }
-
-    /**
-     * SET COLOR
-     * 
-     * @param command
-     */
-    protected void setColor(HSBType command) {
-        HashMap<String, Object> newState = new HashMap<>();
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_HUE, command.getHue().intValue());
-        newState.put(JSON_KEY_SATURATION, command.getSaturation().intValue());
-        newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness().intValue());
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * SET COLORTEMP
-     * 
-     * @param colorTemp (Integer) in Kelvin
-     */
-    protected void setColorTemp(Integer colorTemp) {
-        HashMap<String, Object> newState = new HashMap<>();
-        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_COLORTEMP, colorTemp);
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * set Light Effect from channel/command
-     * 
-     * @param channel channel (effect) to set
-     * @param command command (value) to set
-     */
-    protected void setLightEffect(String channel, Command command) {
-        TapoLightEffect lightEffect = deviceInfo.getLightEffect();
-        switch (channel) {
-            case CHANNEL_FX_BRIGHTNESS:
-                if (command instanceof PercentType percentCommand) {
-                    Float percent = percentCommand.floatValue();
-                    lightEffect.setBrightness(percent.intValue()); // 0..100% = 0..100
-                } else if (command instanceof DecimalType decimalCommand) {
-                    lightEffect.setBrightness(decimalCommand.intValue());
-                }
-                break;
-            case CHANNEL_FX_COLORS:
-                // comming soon
-                break;
-            case CHANNEL_FX_NAME:
-                lightEffect.setName(command.toString());
-                break;
-        }
-        setLightEffects(lightEffect);
-    }
-
-    /**
-     * SET LIGHTNING EFFECTS
-     * 
-     * @param lightEffect new lightEffect
-     */
-    protected void setLightEffects(TapoLightEffect lightEffect) {
-        JsonObject newEffect = new JsonObject();
-        newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_ENABLE, lightEffect.getEnable());
-        newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_NAME, lightEffect.getName());
-        newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS, lightEffect.getBrightness());
-        newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_COLORTEMPRANGE, lightEffect.getColorTempRange().toString());
-        newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_DISPLAYCOLORS, lightEffect.getDisplayColors().toString());
-        newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_CUSTOM, lightEffect.getCustom());
-
-        connector.sendDeviceCommand(JSON_KEY_LIGHTNING_EFFECT, newEffect.toString());
-    }
-
-    /**
-     * UPDATE PROPERTIES
-     * 
-     * @param deviceInfo TapoDeviceInfo
-     */
-    @Override
-    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
-        TapoLightEffect lightEffect = deviceInfo.getLightEffect();
-        super.devicePropertiesChanged(deviceInfo);
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
-                getPercentType(deviceInfo.getBrightness()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
-                getDecimalType(deviceInfo.getColorTemp()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
-                getDecimalType(deviceInfo.getSignalLevel()));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
-                getTimeType(deviceInfo.getOnTime(), Units.SECOND));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
-        // light effect
-        publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_BRIGHTNESS),
-                getPercentType(lightEffect.getBrightness()));
-        publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(lightEffect.getName()));
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartBulb.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartBulb.java
deleted file mode 100644 (file)
index 9db4089..0000000
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoLightEffect;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Smart-Plug-Device.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoSmartBulb extends TapoDevice {
-    private final Logger logger = LoggerFactory.getLogger(TapoSmartBulb.class);
-
-    /**
-     * Constructor
-     * 
-     * @param thing Thing object representing device
-     */
-    public TapoSmartBulb(Thing thing) {
-        super(thing);
-    }
-
-    /**
-     * handle command sent to device
-     * 
-     * @param channelUID channelUID command is sent to
-     * @param command command to be sent
-     */
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        Boolean refreshInfo = false;
-
-        String channel = channelUID.getIdWithoutGroup();
-        if (command instanceof RefreshType) {
-            refreshInfo = true;
-        } else {
-            switch (channel) {
-                case CHANNEL_OUTPUT:
-                    connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
-                    refreshInfo = true;
-                    break;
-                case CHANNEL_BRIGHTNESS:
-                    if (command instanceof PercentType percentCommand) {
-                        Float percent = percentCommand.floatValue();
-                        setBrightness(percent.intValue()); // 0..100% = 0..100
-                        refreshInfo = true;
-                    } else if (command instanceof DecimalType decimalCommand) {
-                        setBrightness(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR_TEMP:
-                    if (command instanceof DecimalType decimalCommand) {
-                        setColorTemp(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR:
-                    if (command instanceof HSBType hsbCommand) {
-                        setColor(hsbCommand);
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_FX_NAME:
-                    setLightEffect(command.toString());
-                    break;
-                default:
-                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
-                            channelUID.getId());
-            }
-        }
-
-        /* refreshInfo */
-        if (refreshInfo) {
-            queryDeviceInfo(true);
-        }
-    }
-
-    /**
-     * SET BRIGHTNESS
-     * 
-     * @param newBrightness percentage 0-100 of new brightness
-     */
-    protected void setBrightness(Integer newBrightness) {
-        /* switch off if 0 */
-        if (newBrightness == 0) {
-            connector.sendDeviceCommand(JSON_KEY_ON, false);
-        } else {
-            HashMap<String, Object> newState = new HashMap<>();
-            newState.put(JSON_KEY_ON, true);
-            newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
-            connector.sendDeviceCommands(newState);
-        }
-    }
-
-    /**
-     * SET COLOR
-     * 
-     * @param command
-     */
-    protected void setColor(HSBType command) {
-        HashMap<String, Object> newState = new HashMap<>();
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_HUE, command.getHue().intValue());
-        newState.put(JSON_KEY_SATURATION, command.getSaturation().intValue());
-        newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness().intValue());
-        newState.put(JSON_KEY_LIGHTNING_DYNAMIC_ENABLE, false);
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * SET COLORTEMP
-     * 
-     * @param colorTemp (Integer) in Kelvin
-     */
-    protected void setColorTemp(Integer colorTemp) {
-        HashMap<String, Object> newState = new HashMap<>();
-        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_COLORTEMP, colorTemp);
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * Set light effect
-     * 
-     * @param fxName (String) id of LightEffect
-     */
-    protected void setLightEffect(String fxName) {
-        HashMap<String, Object> newState = new HashMap<>();
-        if (fxName.length() > 0 && !fxName.equals(JSON_KEY_LIGHTNING_EFFECT_OFF)) {
-            newState.put(JSON_KEY_LIGHTNING_EFFECT_ENABLE, true);
-            newState.put(JSON_KEY_LIGHTNING_EFFECT_ID, fxName);
-        } else {
-            newState.put(JSON_KEY_LIGHTNING_EFFECT_ENABLE, false);
-        }
-        connector.sendDeviceCommands(DEVICE_CMD_SET_LIGHT_FX, newState);
-    }
-
-    /**
-     * UPDATE PROPERTIES
-     * 
-     * @param deviceInfo TapoDeviceInfo
-     */
-    @Override
-    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
-        super.devicePropertiesChanged(deviceInfo);
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
-                getPercentType(deviceInfo.getBrightness()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
-                getDecimalType(deviceInfo.getColorTemp()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
-                getDecimalType(deviceInfo.getSignalLevel()));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
-                getTimeType(deviceInfo.getOnTime(), Units.SECOND));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
-
-        updateLightEffectChannels(deviceInfo.getLightEffect());
-    }
-
-    /**
-     * Set light effect channels
-     * 
-     * @param lightEffect
-     */
-    protected void updateLightEffectChannels(TapoLightEffect lightEffect) {
-        String fxId = "";
-        if (lightEffect.getEnable().equals(true)) {
-            fxId = lightEffect.getId();
-        }
-        publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(fxId));
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java
deleted file mode 100644 (file)
index 410c02f..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Smart-Plug-Device.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoSmartPlug extends TapoDevice {
-    private final Logger logger = LoggerFactory.getLogger(TapoSmartPlug.class);
-
-    /**
-     * Constructor
-     *
-     * @param thing Thing object representing device
-     */
-    public TapoSmartPlug(Thing thing) {
-        super(thing);
-    }
-
-    /**
-     * handle command sent to device
-     *
-     * @param channelUID channelUID command is sent to
-     * @param command command to be sent
-     */
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        boolean refreshInfo = false;
-        String id = channelUID.getIdWithoutGroup();
-
-        /* perform actions */
-        if (command instanceof RefreshType) {
-            refreshInfo = true;
-        } else if (command instanceof OnOffType) {
-            Boolean targetState = command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE;
-            if (CHANNEL_OUTPUT.equals(id)) { // Command is sent to the device output
-                connector.sendDeviceCommand(JSON_KEY_ON, targetState);
-                refreshInfo = true;
-            } else if (id.startsWith(CHANNEL_OUTPUT)) { // Command is sent to a child's device output
-                Integer index = Integer.valueOf(id.replace(CHANNEL_OUTPUT, ""));
-                connector.sendChildCommand(index, JSON_KEY_ON, targetState);
-                refreshInfo = true;
-            }
-        } else {
-            logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
-        }
-
-        /* refreshInfo */
-        if (refreshInfo) {
-            queryDeviceInfo(true);
-        }
-    }
-
-    /**
-     * UPDATE PROPERTIES
-     *
-     * @param deviceInfo TapoDeviceInfo
-     */
-    @Override
-    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
-        super.devicePropertiesChanged(deviceInfo);
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
-                getDecimalType(deviceInfo.getSignalLevel()));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
-                getTimeType(deviceInfo.getOnTime(), Units.SECOND));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java
deleted file mode 100644 (file)
index 547d6ac..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Universal-Device
- * universal device for testing pruposes
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoUniversalDevice extends TapoDevice {
-    private final Logger logger = LoggerFactory.getLogger(TapoUniversalDevice.class);
-
-    // CHANNEL LIST
-    public static final String CHANNEL_GROUP_DEBUG = "debug";
-    public static final String CHANNEL_RESPONSE = "deviceResponse";
-    public static final String CHANNEL_COMMAND = "deviceCommand";
-
-    /**
-     * Constructor
-     *
-     * @param thing Thing object representing device
-     */
-    public TapoUniversalDevice(Thing thing) {
-        super(thing);
-    }
-
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command.toString(), channelUID.getId());
-        Boolean refreshInfo = false;
-
-        String channel = channelUID.getIdWithoutGroup();
-        if (command instanceof RefreshType) {
-            refreshInfo = true;
-        } else {
-            switch (channel) {
-                case CHANNEL_OUTPUT:
-                    connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
-                    refreshInfo = true;
-                    break;
-                case CHANNEL_BRIGHTNESS:
-                    if (command instanceof PercentType percentCommand) {
-                        Float percent = percentCommand.floatValue();
-                        setBrightness(percent.intValue()); // 0..100% = 0..100
-                        refreshInfo = true;
-                    } else if (command instanceof DecimalType decimalCommand) {
-                        setBrightness(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR_TEMP:
-                    if (command instanceof DecimalType decimalCommand) {
-                        setColorTemp(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR:
-                    if (command instanceof HSBType hsbCommand) {
-                        setColor(hsbCommand);
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COMMAND:
-                    String[] cmd = command.toString().split(":");
-                    if (cmd.length == 1) {
-                        connector.sendCustomQuery(cmd[0]);
-                    } else if (cmd.length == 2) {
-                        connector.sendDeviceCommand(cmd[0], cmd[1]);
-                    } else {
-                        logger.warn("({}) wrong command format '{}'", uid, command.toString());
-                    }
-                    break;
-                default:
-                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
-                            channelUID.getId());
-            }
-        }
-
-        /* refreshInfo */
-        if (refreshInfo) {
-            queryDeviceInfo();
-        }
-    }
-
-    /**
-     * SET BRIGHTNESS
-     * 
-     * @param newBrightness percentage 0-100 of new brightness
-     */
-    protected void setBrightness(Integer newBrightness) {
-        /* switch off if 0 */
-        if (newBrightness == 0) {
-            connector.sendDeviceCommand(JSON_KEY_ON, false);
-        } else {
-            HashMap<String, Object> newState = new HashMap<>();
-            newState.put(JSON_KEY_ON, true);
-            newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
-            connector.sendDeviceCommands(newState);
-        }
-    }
-
-    /**
-     * SET COLOR
-     * 
-     * @param command
-     */
-    protected void setColor(HSBType command) {
-        HashMap<String, Object> newState = new HashMap<>();
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_HUE, command.getHue());
-        newState.put(JSON_KEY_SATURATION, command.getSaturation());
-        newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness());
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * SET COLORTEMP
-     * 
-     * @param colorTemp (Integer) in Kelvin
-     */
-    protected void setColorTemp(Integer colorTemp) {
-        HashMap<String, Object> newState = new HashMap<>();
-        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_COLORTEMP, colorTemp);
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * SET DEVICE INFOs to device
-     * 
-     * @param deviceInfo
-     */
-    @Override
-    public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
-        devicePropertiesChanged(deviceInfo);
-        handleConnectionState();
-    }
-
-    /**
-     * Handle full responsebody received from connector
-     * 
-     * @param responseBody
-     */
-    @Override
-    public void responsePasstrough(String responseBody) {
-        logger.debug("({}) received response {}", uid, responseBody);
-        publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
-    }
-
-    /**
-     * UPDATE PROPERTIES
-     * 
-     * @param deviceInfo TapoDeviceInfo
-     */
-    @Override
-    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
-        super.devicePropertiesChanged(deviceInfo);
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
-                getPercentType(deviceInfo.getBrightness()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
-                getDecimalType(deviceInfo.getColorTemp()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
-
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
-                getDecimalType(deviceInfo.getSignalLevel()));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
-                getTimeType(deviceInfo.getOnTime(), Units.SECOND));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
-                getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
-    }
-
-    /***********************************
-     *
-     * CHANNELS
-     *
-     ************************************/
-    /**
-     * Get ChannelID including group
-     * 
-     * @param group String channel-group
-     * @param channel String channel-name
-     * @return String channelID
-     */
-    @Override
-    protected String getChannelID(String group, String channel) {
-        return group + "#" + channel;
-    }
-
-    /**
-     * Get Channel from ChannelID
-     * 
-     * @param channelID String channelID
-     * @return String channel-name
-     */
-    @Override
-    protected String getChannelFromID(ChannelUID channelID) {
-        String channel = channelID.getIdWithoutGroup();
-        channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
-        return channel;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeConfiguration.java
new file mode 100644 (file)
index 0000000..5075e68
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.bridge;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoBridgeConfiguration {
+    /* THING CONFIGUTATION PROPERTYS */
+    public static final String CONFIG_EMAIL = "username";
+    public static final String CONFIG_PASS = "password";
+    public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
+    public static final String CONFIG_DISCOVERY_UDP = "udpDiscovery";
+    public static final String CONFIG_DISCOVERY_ONLINE = "onlyLocalOnlineDevices";
+    public static final String CONFIG_BROADCAST_ADDRESS = "broadcastAddress";
+    public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
+
+    /* DEFAULT & FIXED CONFIGURATIONS */
+    public static final Integer CONFIG_CLOUD_FIXED_INTERVAL = 1440;
+
+    /* thing configuration parameter. */
+    public String username = "";
+    public String password = "";
+    public String broadcastAddress = "255.255.255.255";
+    public boolean cloudDiscovery = false;
+    public boolean udpDiscovery = false;
+    public boolean onlyLocalOnlineDevices = false;
+    public int reconnectInterval = CONFIG_CLOUD_FIXED_INTERVAL;
+    public int discoveryInterval = 60;
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeHandler.java
new file mode 100644 (file)
index 0000000..524b8e0
--- /dev/null
@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.bridge;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
+import org.openhab.binding.tapocontrol.internal.discovery.TapoDiscoveryService;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels with a bridge.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBridgeHandler extends BaseBridgeHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
+    private final TapoErrorHandler bridgeError = new TapoErrorHandler();
+    private TapoBridgeConfiguration config = new TapoBridgeConfiguration();
+    private final HttpClient httpClient;
+    private @Nullable ScheduledFuture<?> startupJob;
+    private @Nullable ScheduledFuture<?> pollingJob;
+    private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
+    private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
+    private TapoCredentials credentials;
+
+    private String uid;
+
+    public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
+        super(bridge);
+        Thing thing = getThing();
+        cloudConnector = new TapoCloudConnector(this);
+        credentials = new TapoCredentials();
+        uid = thing.getUID().toString();
+        this.httpClient = httpClient;
+    }
+
+    /***********************************
+     *
+     * BRIDGE INITIALIZATION
+     *
+     ************************************/
+    @Override
+    /**
+     * INIT BRIDGE
+     * set credentials and login cloud
+     */
+    public void initialize() {
+        config = getConfigAs(TapoBridgeConfiguration.class);
+        credentials = new TapoCredentials(config.username, config.password);
+        activateBridge();
+    }
+
+    /**
+     * ACTIVATE BRIDGE
+     */
+    private void activateBridge() {
+        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+        updateStatus(ThingStatus.UNKNOWN);
+
+        // background initialization (delay it a little bit):
+        this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
+    }
+
+    @Override
+    public void dispose() {
+        stopScheduler(this.startupJob);
+        stopScheduler(this.pollingJob);
+        super.dispose();
+    }
+
+    /**
+     * ACTIVATE DISCOVERY SERVICE
+     */
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(TapoDiscoveryService.class);
+    }
+
+    /**
+     * Set DiscoveryService
+     * 
+     * @param discoveryService
+     */
+    public void setDiscoveryService(TapoDiscoveryService discoveryService) {
+        this.discoveryService = discoveryService;
+    }
+
+    /***********************************
+     *
+     * SCHEDULER
+     *
+     ************************************/
+
+    /**
+     * delayed OneTime StartupJob
+     */
+    private void delayedStartUp() {
+        loginCloud();
+        startCloudScheduler();
+        discoveryService.startBackgroundDiscovery();
+    }
+
+    /**
+     * Start CloudLogin Scheduler
+     */
+    protected void startCloudScheduler() {
+        int pollingInterval = config.reconnectInterval;
+        TimeUnit timeUnit = TimeUnit.MINUTES;
+        if (pollingInterval > 0) {
+            logger.debug("{} starting cloudScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
+
+            this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
+                    timeUnit);
+        } else {
+            logger.debug("({}) cloudScheduler disabled with config '0'", uid);
+            stopScheduler(this.pollingJob);
+        }
+    }
+
+    /**
+     * Stop scheduler
+     * 
+     * @param scheduler ScheduledFeature which should be stopped
+     */
+    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+        if (scheduler != null) {
+            scheduler.cancel(true);
+            scheduler = null;
+        }
+    }
+
+    /***********************************
+     *
+     * ERROR HANDLER
+     *
+     ************************************/
+
+    /**
+     * return device Error
+     * 
+     * @return
+     */
+    public TapoErrorHandler getErrorHandler() {
+        return bridgeError;
+    }
+
+    /**
+     * set device error
+     * 
+     * @param tapoError TapoErrorHandler-Object
+     */
+    public void setError(TapoErrorHandler tapoError) {
+        bridgeError.set(tapoError);
+        handleConnectionState();
+    }
+
+    /***********************************
+     *
+     * BRIDGE COMMUNICATIONS
+     *
+     ************************************/
+
+    /**
+     * Login to Cloud
+     * 
+     * @return
+     */
+    public boolean loginCloud() {
+        bridgeError.reset(); // reset ErrorHandler
+        if (credentials.areSet()) {
+            try {
+                cloudConnector.login(credentials);
+            } catch (Exception e) {
+                logger.trace("({}) login to cloud failed", this.uid);
+            }
+        } else {
+            bridgeError.raiseError(ERR_BINDING_CREDENTIALS, "credentials not set");
+        }
+        handleConnectionState();
+        return cloudConnector.isLoggedIn();
+    }
+
+    /**
+     * Handle Connection state
+     */
+    private void handleConnectionState() {
+        if (cloudConnector.isLoggedIn() && !bridgeError.hasError()) {
+            updateStatus(ThingStatus.ONLINE);
+        } else if (bridgeError.hasError()) {
+            switch (bridgeError.getType()) {
+                case COMMUNICATION_ERROR:
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
+                    break;
+                case CONFIGURATION_ERROR:
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, bridgeError.getMessage());
+                    break;
+                default:
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, bridgeError.getMessage());
+            }
+        } else {
+            updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
+        }
+    }
+
+    /***********************************
+     *
+     * BRIDGE GETTERS
+     *
+     ************************************/
+
+    public TapoCredentials getCredentials() {
+        return credentials;
+    }
+
+    public HttpClient getHttpClient() {
+        return httpClient;
+    }
+
+    public TapoCloudConnector getCloudConnector() {
+        return cloudConnector;
+    }
+
+    public TapoDiscoveryService getDiscoveryService() {
+        return discoveryService;
+    }
+
+    public ThingUID getUID() {
+        return getThing().getUID();
+    }
+
+    public TapoBridgeConfiguration getBridgeConfig() {
+        return config;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginData.java
new file mode 100644 (file)
index 0000000..c9d6760
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.bridge.dto;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+
+import java.util.UUID;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * TapoCloudLoginData Record for sending request
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoCloudLoginData(@Expose String appType, @Expose String cloudUserName, @Expose String cloudPassword,
+        @Expose String terminalUUID) {
+
+    public TapoCloudLoginData(String cloudUserName, String cloudPassword) {
+        this(TAPO_APP_TYPE, cloudUserName, cloudPassword, UUID.randomUUID().toString());
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginResult.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginResult.java
new file mode 100644 (file)
index 0000000..0180d6a
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.bridge.dto;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * TapoCloudLogin Result Class as record
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoCloudLoginResult(@Expose @SerializedName("accountId") String accountId,
+        @Expose @SerializedName("regTime") String regTime, @Expose @SerializedName("countryCode") String countryCode,
+        @Expose @SerializedName("riskDetected") int riskDetected, @Expose @SerializedName("nickname") String nickname,
+        @Expose @SerializedName("email") String email, @Expose @SerializedName("token") String token) {
+
+    /* init new emty record */
+    public TapoCloudLoginResult() {
+        this("", "", "", 0, "", "", "");
+    }
+
+    /**********************************************
+     * Return default data if recordobject is null
+     **********************************************/
+    @Override
+    public String accountId() {
+        return Objects.requireNonNullElse(accountId, "");
+    }
+
+    @Override
+    public String token() {
+        return Objects.requireNonNullElse(token, "");
+    }
+
+    @Override
+    public String email() {
+        return Objects.requireNonNullElse(email, "");
+    }
+
+    @Override
+    public String nickname() {
+        return Objects.requireNonNullElse(nickname, "");
+    }
+
+    @Override
+    public int riskDetected() {
+        return Objects.requireNonNullElse(riskDetected, 0);
+    }
+
+    @Override
+    public String countryCode() {
+        return Objects.requireNonNullElse(countryCode, "");
+    }
+
+    @Override
+    public String regTime() {
+        return Objects.requireNonNullElse(regTime, "");
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoBaseDeviceData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoBaseDeviceData.java
new file mode 100644 (file)
index 0000000..9bc98ab
--- /dev/null
@@ -0,0 +1,159 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.dto;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Base-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBaseDeviceData {
+    @SerializedName("device_id")
+    @Expose(serialize = false, deserialize = true)
+    private String deviceId = "";
+
+    @SerializedName("fw_ver")
+    @Expose(serialize = false, deserialize = true)
+    private String fwVer = "";
+
+    @SerializedName("hw_ver")
+    @Expose(serialize = false, deserialize = true)
+    private String hwVer = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String mac = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String model = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String nickname = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String region = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String type = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String lang = "";
+
+    @SerializedName("hw_id")
+    @Expose(serialize = false, deserialize = true)
+    private String hwId = "";
+
+    @SerializedName("fw_id")
+    @Expose(serialize = false, deserialize = true)
+    private String fwId = "";
+
+    @SerializedName("oem_id")
+    @Expose(serialize = false, deserialize = true)
+    private String oemId = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private String ip = "";
+
+    @SerializedName(value = "overheated", alternate = "overheatStatus")
+    @Expose(serialize = false, deserialize = true)
+    private boolean overheated = false;
+
+    @Expose(serialize = false, deserialize = true)
+    private int rssi = 0;
+
+    @SerializedName("signal_level")
+    @Expose(serialize = false, deserialize = true)
+    private int signalLevel = 0;
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public String getFirmwareVersion() {
+        return fwVer;
+    }
+
+    public String getFirmwareId() {
+        return fwId;
+    }
+
+    public String getHardwareVersion() {
+        return hwVer;
+    }
+
+    public String getHardwareId() {
+        return hwId;
+    }
+
+    public boolean isOverheated() {
+        return overheated;
+    }
+
+    public String getLanguage() {
+        return lang;
+    }
+
+    public String getMAC() {
+        return formatMac(mac, MAC_DIVISION_CHAR);
+    }
+
+    public String getModel() {
+        return model.replace(" Series", "");
+    }
+
+    public String getNickname() {
+        return nickname;
+    }
+
+    public String getOEM() {
+        return oemId;
+    }
+
+    public String getRegion() {
+        return region;
+    }
+
+    public String getRepresentationProperty() {
+        return getMAC();
+    }
+
+    public int getSignalLevel() {
+        return signalLevel;
+    }
+
+    public int getRSSI() {
+        return rssi;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public String getIpAddress() {
+        return ip;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildDeviceData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildDeviceData.java
new file mode 100644 (file)
index 0000000..491c78b
--- /dev/null
@@ -0,0 +1,202 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo Child Device Information class
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Christian Wild - Integrating TapoHub
+ */
+@NonNullByDefault
+public class TapoChildDeviceData extends TapoBaseDeviceData {
+    @SerializedName("at_low_battery")
+    @Expose(serialize = false, deserialize = true)
+    private boolean atLowBattery = false;
+
+    @SerializedName("battery_percentage")
+    @Expose(serialize = false, deserialize = true)
+    private int batteryPercentage = 0;
+
+    @SerializedName("bind_count")
+    @Expose(serialize = false, deserialize = true)
+    private int bindCount = 0;
+
+    @SerializedName("temp_unit")
+    @Expose(serialize = false, deserialize = true)
+    private String tempUnit = "celsius";
+
+    @SerializedName("current_temp")
+    @Expose(serialize = false, deserialize = true)
+    private double currentTemp = 0.0;
+
+    @SerializedName("current_humidity")
+    @Expose(serialize = false, deserialize = true)
+    private int currentHumidity = 0;
+
+    @Expose(serialize = false, deserialize = true)
+    private String category = "";
+
+    @SerializedName("device_on")
+    @Expose(serialize = true, deserialize = true)
+    private boolean deviceOn = false;
+
+    @SerializedName("jamming_rssi")
+    @Expose(serialize = false, deserialize = true)
+    private int jammingRssi = 0;
+
+    @SerializedName("jamming_signal_level")
+    @Expose(serialize = false, deserialize = true)
+    private int jammingSignalLevel = 0;
+
+    @SerializedName("lastOnboardingTimestamp")
+    @Expose(serialize = false, deserialize = true)
+    private long lastOnboardingTimestamp = 0;
+
+    @SerializedName("on_time")
+    @Expose(serialize = false, deserialize = true)
+    private long onTime = 0;
+
+    @Expose(serialize = false, deserialize = true)
+    private boolean open = false;
+
+    @SerializedName("parent_device_id")
+    @Expose(serialize = false, deserialize = true)
+    private String parentDeviceId = "";
+
+    @Expose(serialize = false, deserialize = true)
+    private int position = 0;
+
+    @SerializedName("report_interval")
+    @Expose(serialize = false, deserialize = true)
+    private int reportInterval = 0;
+
+    @SerializedName("slot_number")
+    @Expose(serialize = false, deserialize = true)
+    private int slotNumber = 0;
+
+    @Expose(serialize = false, deserialize = true)
+    private String status = "";
+
+    @SerializedName("status_follow_edge")
+    @Expose(serialize = false, deserialize = true)
+    private boolean statusFollowEedge = false;
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    /* get boolean values */
+    public boolean batteryIsLow() {
+        return atLowBattery;
+    }
+
+    public boolean getStatusFollowEedge() {
+        return statusFollowEedge;
+    }
+
+    public int getBatteryPercentage() {
+        return batteryPercentage;
+    }
+
+    public boolean isOff() {
+        return !deviceOn;
+    }
+
+    public boolean isOn() {
+        return deviceOn;
+    }
+
+    public double getTemperature() {
+        return currentTemp;
+    }
+
+    public String getTempUnit() {
+        return tempUnit;
+    }
+
+    public int getHumidity() {
+        return currentHumidity;
+    }
+
+    public boolean isOnline() {
+        return "online".equals(status);
+    }
+
+    public boolean isOpen() {
+        return open;
+    }
+
+    /* get numeric values */
+    public int getBindCount() {
+        return bindCount;
+    }
+
+    public int getJammingRssi() {
+        return jammingRssi;
+    }
+
+    public int getJammingSignalLevel() {
+        return jammingSignalLevel;
+    }
+
+    public int getPosition() {
+        return position;
+    }
+
+    public int getReportInterval() {
+        return reportInterval;
+    }
+
+    public int getSlotNumber() {
+        return slotNumber;
+    }
+
+    public long getLastOnboardingTimestamp() {
+        return lastOnboardingTimestamp;
+    }
+
+    public Number getOnTime() {
+        return onTime;
+    }
+
+    /* get string values */
+    public String getCategory() {
+        return category;
+    }
+
+    public String getParentDeviceId() {
+        return parentDeviceId;
+    }
+
+    @Override
+    public String getRepresentationProperty() {
+        return getDeviceId();
+    }
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+    public void setDeviceOn(boolean deviceOn) {
+        this.deviceOn = deviceOn;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildList.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildList.java
new file mode 100644 (file)
index 0000000..2470d24
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.dto;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Child Structure Class
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class TapoChildList {
+    @Expose
+    @SerializedName("start_index")
+    private int startIndex = 0;
+
+    @Expose
+    private int sum = 0;
+
+    @Expose
+    @SerializedName("child_device_list")
+    private List<TapoChildDeviceData> childDeviceList = List.of();
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public int getStartIndex() {
+        return startIndex;
+    }
+
+    public int getSum() {
+        return sum;
+    }
+
+    public List<TapoChildDeviceData> getChildDeviceList() {
+        return childDeviceList;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoEnergyData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoEnergyData.java
new file mode 100644 (file)
index 0000000..7d2a9dc
--- /dev/null
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.dto;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Energy-Monitor Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoEnergyData {
+    @SerializedName("local_time")
+    @Expose(serialize = false, deserialize = true)
+    private String localTime = "";
+
+    @SerializedName("current_power")
+    @Expose(serialize = false, deserialize = true)
+    private int currentPower = 0;
+
+    @SerializedName("electricity_charge")
+    @Expose(serialize = false, deserialize = true)
+    private List<Integer> electricityCharge = List.of();
+
+    @SerializedName("today_runtime")
+    @Expose(serialize = false, deserialize = true)
+    private int todayRuntime = 0;
+
+    @SerializedName("today_energy")
+    @Expose(serialize = false, deserialize = true)
+    private int todayEnergy = 0;
+
+    @SerializedName("month_energy")
+    @Expose(serialize = false, deserialize = true)
+    private int monthEnergy = 0;
+
+    @SerializedName("month_runtime")
+    @Expose(serialize = false, deserialize = true)
+    private int monthRuntime = 0;
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public ZonedDateTime getLocalDate() {
+        return ZonedDateTime.parse(localTime);
+    }
+
+    public double getCurrentPower() {
+        return (double) currentPower / 1000;
+    }
+
+    public List<Integer> getElectricityCharge() {
+        return electricityCharge;
+    }
+
+    public int getTodayEnergy() {
+        return todayEnergy;
+    }
+
+    public int getMonthEnergy() {
+        return monthEnergy;
+    }
+
+    public int getTodayRuntime() {
+        return todayRuntime;
+    }
+
+    public int getMonthRuntime() {
+        return monthRuntime;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightDynamicFx.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightDynamicFx.java
new file mode 100644 (file)
index 0000000..768734c
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Tapo-DynamicLightEffects Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightDynamicFx {
+    private static final String FX_JSON_FILE = "/lightningfx/dynamic_light_fx.json";
+
+    @Expose
+    private boolean enable = false;
+
+    @Expose
+    private String id = JSON_KEY_LIGHTNING_EFFECT_OFF;
+
+    @Expose(serialize = false, deserialize = true)
+    private String name = "";
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    public void setEffect(String id) throws TapoErrorHandler {
+        setEffect(getEffect(id));
+    }
+
+    public void setEffect(TapoLightDynamicFx effect) {
+        enable = effect.enable;
+        id = effect.id;
+        name = effect.name;
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public boolean isEnabled() {
+        return enable;
+    }
+
+    public String getId() {
+        if (!isEnabled()) {
+            return JSON_KEY_LIGHTNING_EFFECT_OFF;
+        }
+        return id;
+    }
+
+    public String getName() {
+        if (!isEnabled()) {
+            return JSON_KEY_LIGHTNING_EFFECT_OFF;
+        }
+        return name;
+    }
+
+    public boolean hasEffect(String id) {
+        try {
+            return getEffect(id) instanceof TapoLightDynamicFx;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /***********************************
+     *
+     * PRIVATE HELPERS
+     *
+     ************************************/
+
+    /**
+     * Load Dynamic Light FX-Data from JSON
+     * 
+     * @param id id off effect
+     */
+    private TapoLightDynamicFx getEffect(String id) throws TapoErrorHandler {
+        List<TapoLightDynamicFx> effects = getEffectList();
+        for (TapoLightDynamicFx fx : effects) {
+            if (fx.id.equals(id)) {
+                return fx;
+            }
+        }
+        throw new TapoErrorHandler(TapoErrorCode.ERR_BINDING_FX_NOT_FOUND);
+    }
+
+    /**
+     * Load All Effects as List from JSON
+     */
+    private List<TapoLightDynamicFx> getEffectList() throws TapoErrorHandler {
+        InputStream is = getClass().getResourceAsStream(FX_JSON_FILE);
+        if (is != null) {
+            try {
+                Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
+                Type listType = new TypeToken<ArrayList<TapoLightDynamicFx>>() {
+                }.getType();
+                return GSON.fromJson(reader, listType);
+            } catch (Exception e) {
+                throw new TapoErrorHandler(TapoErrorCode.ERR_API_JSON_DECODE_FAIL);
+            }
+        } else {
+            throw new TapoErrorHandler(TapoErrorCode.ERR_BINDING_FX_NOT_FOUND);
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightEffect.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightEffect.java
new file mode 100644 (file)
index 0000000..184c9d4
--- /dev/null
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-LightEffects Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightEffect {
+    @Expose
+    private String id = "";
+
+    @Expose
+    private String name = "";
+
+    @Expose
+    private Integer brightness = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("display_colors")
+    private List<int[]> displayColors = List.of();
+
+    @Expose
+    private Integer enable = 0;
+
+    @Expose
+    @Nullable
+    private Integer bAdjusted = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("brightness_range")
+    private Integer[] brightnessRange = {};
+
+    @Expose
+    @Nullable
+    private List<int[]> backgrounds = List.of();
+
+    @Expose
+    @Nullable
+    private Integer custom = 0;
+
+    @Expose
+    @Nullable
+    private Integer direction = 0;
+
+    @Expose
+    @Nullable
+    private Integer duration = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("expansion_strategy")
+    private Integer expansionStrategy = 0;
+
+    @Expose
+    @Nullable
+    private Integer fadeoff = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("hue_range")
+    private Integer[] hueRange = {};
+
+    @SerializedName("init_states")
+    @Expose
+    @Nullable
+    private List<Integer[]> initStates = List.of();
+    @Expose
+    @Nullable
+    @SerializedName("random_seed")
+    private Integer randomSeed = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("repeat_times")
+    private Integer repeatTimes = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("saturation_range")
+    private Integer[] saturationRange = {};
+
+    @Expose
+    @Nullable
+    @SerializedName("segment_length")
+    private Integer segmentLength = 0;
+
+    @Expose
+    @Nullable
+    private Integer[] segments = {};
+
+    @Expose
+    @Nullable
+    private List<int[]> sequence = List.of();
+
+    @Expose
+    @Nullable
+    private Integer spread = 0;
+
+    @Expose
+    @Nullable
+    private Integer transition = 0;
+
+    @Expose
+    @Nullable
+    @SerializedName("transition_range")
+    private Integer[] transitionRange = {};
+
+    @Expose
+    @Nullable
+    private String type = "";
+
+    @Expose
+    @Nullable
+    @SerializedName("trans_sequence")
+    private List<int[]> transSequence = List.of();
+
+    @Expose
+    @Nullable
+    @SerializedName("run_time")
+    private Integer runTime = 0;
+
+    /**
+     * Init class with effect id
+     */
+    public TapoLightEffect(boolean enable, String fxId) {
+        setEnable(enable);
+        id = fxId;
+    }
+
+    public TapoLightEffect(String fxId) {
+        setEnable((fxId.length() > 0 && !fxId.equals(JSON_KEY_LIGHTNING_EFFECT_OFF)));
+        id = fxId;
+    }
+
+    public TapoLightEffect() {
+    }
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    public void setEnable(boolean enable) {
+        this.enable = (enable) ? 1 : 0;
+    }
+
+    public void setBrightness(Integer value) {
+        brightness = value;
+    }
+
+    /**
+     * set light fx from fx-name
+     * loads fx data from resources/lightningfx/[fxname].json
+     * 
+     * @param fxName name of effect
+     * @return
+     */
+    public TapoLightEffect setEffect(String fxName) throws TapoErrorHandler {
+        if (JSON_KEY_LIGHTNING_EFFECT_OFF.equals(fxName)) {
+            enable = 0;
+            return this;
+        } else {
+            InputStream is = getClass().getResourceAsStream("/lightningfx/" + fxName + ".json");
+            if (is != null) {
+                try {
+                    Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
+                    return GSON.fromJson(reader, TapoLightEffect.class);
+                } catch (Exception e) {
+                    throw new TapoErrorHandler(TapoErrorCode.ERR_API_JSON_DECODE_FAIL, fxName);
+                }
+            } else {
+                throw new TapoErrorHandler(TapoErrorCode.ERR_BINDING_FX_NOT_FOUND, fxName);
+            }
+        }
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public boolean isEnabled() {
+        return enable == 1;
+    }
+
+    public String getName() {
+        if (!isEnabled()) {
+            return JSON_KEY_LIGHTNING_EFFECT_OFF;
+        }
+        return name;
+    }
+
+    public Integer getBrightness() {
+        return brightness;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/TapoChildDeviceHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/TapoChildDeviceHandler.java
new file mode 100644 (file)
index 0000000..6ec04a8
--- /dev/null
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.rf;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.hub.TapoHubHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Basic Child-Device-Handler
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public abstract class TapoChildDeviceHandler extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoChildDeviceHandler.class);
+    protected final TapoErrorHandler deviceError = new TapoErrorHandler();
+    protected final String uid;
+    protected final String deviceId;
+    protected @NonNullByDefault({}) TapoHubHandler hub;
+    private TapoChildDeviceData deviceInfo = new TapoChildDeviceData();
+    private Map<String, Object> oldStates = new HashMap<>();
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    protected TapoChildDeviceHandler(Thing thing) {
+        super(thing);
+        uid = getThing().getUID().getAsString();
+        deviceId = getValueOrDefault(getThing().getProperties().get(CHILD_REPRESENTATION_PROPERTY), "");
+    }
+
+    /***********************************
+     *
+     * INIT AND SETTINGS
+     *
+     ************************************/
+
+    /**
+     * INITIALIZE DEVICE
+     */
+    @Override
+    public void initialize() {
+        logger.trace("({}) Initializing thing ", uid);
+        Bridge bridgeThing = getBridge();
+        if (bridgeThing != null) {
+            if (bridgeThing.getHandler() instanceof TapoHubHandler tapoHubHandler) {
+                this.hub = tapoHubHandler;
+            }
+            activateDevice();
+        } else {
+            deviceError.raiseError(ERR_CONFIG_NO_BRIDGE);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
+        }
+    }
+
+    /**
+     * ACTIVATE DEVICE
+     */
+    private void activateDevice() {
+        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+        updateStatus(ThingStatus.UNKNOWN);
+
+        if (hub.getChild(deviceId).isOnline()) {
+            updateStatus(ThingStatus.ONLINE);
+        } else {
+            updateStatus(ThingStatus.OFFLINE);
+        }
+    }
+
+    /**
+     * handle command sent to device
+     *
+     * @param channelUID channelUID command is sent to
+     * @param command command to be sent
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        /* perform actions */
+        if (command instanceof RefreshType) {
+            setDeviceData();
+        } else {
+            logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+        }
+    }
+
+    /***********************************
+     *
+     * DEVICE PROPERTIES
+     *
+     ************************************/
+
+    /**
+     * refresh child properties and data from hub data
+     */
+    public void setDeviceData() {
+        setDeviceData(hub.getChild(deviceId));
+    }
+
+    /*
+     * refresh child properties and data from hub data
+     */
+    public void setDeviceData(TapoChildDeviceData deviceInfo) {
+        this.deviceInfo = deviceInfo;
+        triggerEvents(deviceInfo);
+        devicePropertiesChanged(deviceInfo);
+        handleConnectionState();
+    }
+
+    /**
+     * handle device state by connector error
+     */
+    private void handleConnectionState() {
+        if (deviceInfo.isOnline()) {
+            updateStatus(ThingStatus.ONLINE);
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+        }
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     *
+     * @param deviceInfo ChildDeviceData
+     */
+    protected void devicePropertiesChanged(TapoChildDeviceData deviceInfo) {
+        logger.trace("({}) devicePropertiesChanged ", uid);
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_SIGNAL_STRENGTH),
+                getDecimalType(deviceInfo.getSignalLevel()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_BATTERY_LOW), getOnOffType(deviceInfo.batteryIsLow()));
+    }
+
+    /**
+     * Fires events on {@link TapoChildDeviceData} changes.
+     */
+    protected void triggerEvents(TapoChildDeviceData deviceInfo) {
+        if (checkForStateChange(CHANNEL_BATTERY_LOW, deviceInfo.batteryIsLow())) {
+            if (deviceInfo.batteryIsLow()) {
+                triggerChannel(getChannelID(CHANNEL_GROUP_DEVICE, EVENT_BATTERY_LOW), EVENT_STATE_BATTERY_LOW);
+            }
+        }
+    }
+
+    /***********************************
+     *
+     * CHANNELS
+     *
+     ************************************/
+    /**
+     * Get ChannelID including group
+     *
+     * @param group String channel-group
+     * @param channel String channel-name
+     * @return String channelID
+     */
+    protected String getChannelID(String group, String channel) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+        if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
+            return group + "#" + channel;
+        }
+        return channel;
+    }
+
+    /**
+     * Checks if the state changed since the last channel update.
+     *
+     * @param stateName the name of the state (channel)
+     * @param comparator comparison value
+     * @return <code>true</code> if changed, <code>false</code> if not or no old value exists
+     */
+    protected boolean checkForStateChange(String stateName, Object comparator) {
+        if (oldStates.get(stateName) == null) {
+            oldStates.put(stateName, comparator);
+        } else if (!comparator.equals(oldStates.get(stateName))) {
+            oldStates.put(stateName, comparator);
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/smartcontact/TapoSmartContactHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/smartcontact/TapoSmartContactHandler.java
new file mode 100644 (file)
index 0000000..60c15bf
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.rf.smartcontact;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.rf.TapoChildDeviceHandler;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Contact-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSmartContactHandler extends TapoChildDeviceHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoSmartContactHandler.class);
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    public TapoSmartContactHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Update properties
+     */
+    @Override
+    protected void devicePropertiesChanged(TapoChildDeviceData deviceInfo) {
+        super.devicePropertiesChanged(deviceInfo);
+        updateState(getChannelID(CHANNEL_GROUP_SENSOR, CHANNEL_IS_OPEN), getOnOffType(deviceInfo.isOpen()));
+    }
+
+    /**
+     * Fires events on {@link TapoChildDeviceData} changes.
+     */
+    @Override
+    protected void triggerEvents(TapoChildDeviceData deviceInfo) {
+        super.triggerEvents(deviceInfo);
+        if (checkForStateChange(CHANNEL_IS_OPEN, deviceInfo.isOpen())) {
+            if (deviceInfo.isOpen()) {
+                triggerChannel(getChannelID(CHANNEL_GROUP_SENSOR, EVENT_CONTACT_OPENED), EVENT_STATE_OPENED);
+                logger.trace("({}) contact event fired '{}'", uid, EVENT_STATE_OPENED);
+            } else {
+                triggerChannel(getChannelID(CHANNEL_GROUP_SENSOR, EVENT_CONTACT_CLOSED), EVENT_STATE_CLOSED);
+                logger.trace("({}) contact event fired '{}'", uid, EVENT_STATE_CLOSED);
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/wheatersensor/TapoWheaterSensorHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/wheatersensor/TapoWheaterSensorHandler.java
new file mode 100644 (file)
index 0000000..2616b84
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.rf.wheatersensor;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.rf.TapoChildDeviceHandler;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.Thing;
+
+/**
+ * TAPO Smart-Contact-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoWheaterSensorHandler extends TapoChildDeviceHandler {
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    public TapoWheaterSensorHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Update properties
+     */
+    @Override
+    protected void devicePropertiesChanged(TapoChildDeviceData deviceInfo) {
+        super.devicePropertiesChanged(deviceInfo);
+
+        updateState(getChannelID(CHANNEL_GROUP_SENSOR, CHANNEL_TEMPERATURE),
+                getTemperatureType(deviceInfo.getTemperature(), SIUnits.CELSIUS));
+        updateState(getChannelID(CHANNEL_GROUP_SENSOR, CHANNEL_HUMIDITY), getPercentType(deviceInfo.getHumidity()));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoBaseDeviceHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoBaseDeviceHandler.java
new file mode 100644 (file)
index 0000000..b44d2b0
--- /dev/null
@@ -0,0 +1,543 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoEnergyData;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class as base for TAPO-Device device implementations.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public abstract class TapoBaseDeviceHandler extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoBaseDeviceHandler.class);
+    protected final TapoErrorHandler deviceError = new TapoErrorHandler();
+    protected final String uid;
+    protected TapoDeviceConfiguration deviceConfig = new TapoDeviceConfiguration();
+    protected TapoBaseDeviceData baseDeviceData = new TapoBaseDeviceData();
+    protected TapoEnergyData energyData = new TapoEnergyData();
+    protected @Nullable ScheduledFuture<?> startupJob;
+    protected @Nullable ScheduledFuture<?> pollingJob;
+    protected @NonNullByDefault({}) TapoDeviceConnector connector;
+    protected @NonNullByDefault({}) TapoBridgeHandler bridge;
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    protected TapoBaseDeviceHandler(Thing thing) {
+        super(thing);
+        this.uid = getThing().getUID().getAsString();
+    }
+
+    /***********************************
+     * INIT AND SETTINGS
+     ************************************/
+
+    /**
+     * INITIALIZE DEVICE
+     */
+    @Override
+    public void initialize() {
+        try {
+            deviceConfig = getConfigAs(TapoDeviceConfiguration.class);
+            initConnector();
+            if (checkRequirements()) {
+                activateDevice();
+            }
+        } catch (TapoErrorHandler te) {
+            logger.warn("({}) deviceConfiguration error : {}", uid, te.getMessage());
+            setError(te);
+        } catch (Exception e) {
+            logger.debug("({}) error initializing device : {}", uid, e.getMessage());
+        }
+    }
+
+    /**
+     * Init TapoBridgeHandler TapoDeviceConnector
+     */
+    protected void initConnector() {
+        Bridge bridgeThing = getBridge();
+        if (bridgeThing != null) {
+            BridgeHandler bridgeHandler = bridgeThing.getHandler();
+            if (bridgeHandler instanceof TapoBridgeHandler tapoBridgeHandler) {
+                this.bridge = tapoBridgeHandler;
+                this.connector = new TapoDeviceConnector(this, bridge);
+            }
+        }
+    }
+
+    /**
+     * DISPOSE
+     */
+    @Override
+    public void dispose() {
+        try {
+            stopScheduler(this.startupJob);
+            stopScheduler(this.pollingJob);
+            connector.logout();
+        } catch (Exception e) {
+            // handle exception
+        }
+        super.dispose();
+    }
+
+    /**
+     * ACTIVATE DEVICE
+     */
+    protected void activateDevice() {
+        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+        updateStatus(ThingStatus.UNKNOWN);
+
+        // background initialization (delay it a little bit):
+        this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Check if bridge is set
+     */
+    protected boolean checkBridge() throws TapoErrorHandler {
+        /* check bridge */
+        if (!(bridge instanceof TapoBridgeHandler)) {
+            throw new TapoErrorHandler(ERR_CONFIG_NO_BRIDGE);
+        }
+        /* check credentials */
+        if (!bridge.getCredentials().areSet()) {
+            throw new TapoErrorHandler(ERR_CONFIG_CREDENTIALS);
+        }
+        return true;
+    }
+
+    /**
+     * Check if Bridge is set and deviceConfiguration is set
+     */
+    protected boolean checkRequirements() throws TapoErrorHandler {
+        return (checkBridge() && deviceConfig.checkConfig());
+    }
+
+    /**
+     * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
+     *
+     * @throws IOException if an error code was set in the response object
+     */
+    protected void checkErrors() throws IOException {
+        final Integer errorCode = deviceError.getCode();
+
+        if (errorCode != 0) {
+            throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
+        }
+    }
+
+    /***********************************
+     * SCHEDULER
+     ************************************/
+    /**
+     * delayed OneTime StartupJob
+     */
+    private void delayedStartUp() {
+        connect();
+        startPollingScheduler();
+    }
+
+    /**
+     * Start scheduler
+     */
+    protected void startPollingScheduler() {
+        int pollingInterval = deviceConfig.pollingInterval;
+        TimeUnit timeUnit = TimeUnit.SECONDS;
+
+        if (pollingInterval > 0) {
+            if (pollingInterval < POLLING_MIN_INTERVAL_S) {
+                pollingInterval = POLLING_MIN_INTERVAL_S;
+            }
+            logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
+            this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
+                    pollingInterval, timeUnit);
+        } else {
+            logger.debug("({}) scheduler disabled with deviceConfig '0'", uid);
+            stopScheduler(this.pollingJob);
+        }
+    }
+
+    /**
+     * Stop scheduler
+     *
+     * @param scheduler ScheduledFeature which should be stopped
+     */
+    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+        if (scheduler != null) {
+            scheduler.cancel(true);
+        }
+    }
+
+    /**
+     * Scheduler Action
+     */
+    protected void pollingSchedulerAction() {
+        logger.trace("({}) schedulerAction", uid);
+        queryDeviceData();
+    }
+
+    /***********************************
+     * ERROR HANDLER
+     ************************************/
+    /**
+     * return device Error
+     */
+    public TapoErrorHandler getErrorHandler() {
+        return deviceError;
+    }
+
+    public TapoErrorCode getError() {
+        return deviceError.getError();
+    }
+
+    /**
+     * set device error
+     *
+     * @param tapoError TapoErrorHandler-Object
+     */
+    public void setError(TapoErrorHandler tapoError) {
+        deviceError.set(tapoError);
+        handleConnectionState();
+    }
+
+    /***********************************
+     * THING
+     ************************************/
+
+    /***
+     * Check if ThingType is model
+     *
+     * @param model
+     * @return
+     */
+    protected Boolean isThingModel(String model) {
+        try {
+            model = getDeviceModel(model);
+            ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model);
+            ThingTypeUID expectedType = getThing().getThingTypeUID();
+            return expectedType.equals(foundType);
+        } catch (Exception e) {
+            logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
+     * Compare MAC-Adress
+     *
+     * @param baseDeviceData basebaseDeviceData
+     * @return true if is the expected device
+     */
+    protected boolean isExpectedThing(TapoBaseDeviceData baseDeviceData) {
+        try {
+            String expectedThingUID = getThing().getProperties().get(DEVICE_REPRESENTATION_PROPERTY);
+            String foundThingUID = baseDeviceData.getRepresentationProperty();
+            String foundModel = baseDeviceData.getModel();
+            if (expectedThingUID == null || expectedThingUID.isBlank()) {
+                return isThingModel(foundModel);
+            }
+            /* sometimes received mac was with and sometimes without "-" from device */
+            expectedThingUID = unformatMac(expectedThingUID);
+            foundThingUID = unformatMac(foundThingUID);
+            return expectedThingUID.equals(foundThingUID);
+        } catch (Exception e) {
+            logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Return ThingUID
+     */
+    public ThingUID getThingUID() {
+        return getThing().getUID();
+    }
+
+    /**
+     * Return ipAdress
+     */
+    public String getIpAddress() {
+        return deviceConfig.ipAddress;
+    }
+
+    /*
+     * return device configuration
+     */
+    public TapoDeviceConfiguration getDeviceConfig() {
+        return deviceConfig;
+    }
+
+    /***********************************
+     * DEVICE PROPERTIES
+     ************************************/
+
+    /**
+     * query default device properties
+     * query baseDeviceData, energyData (if available for device) and childData (if available for device).
+     */
+    public void queryDeviceData() {
+        queryDeviceData(false);
+    }
+
+    /**
+     * query default device properties
+     * query baseDeviceData, energyData (if available for device)
+     * 
+     * @param ignoreGap ignore gap to last query. query anyway (force)
+     */
+    public void queryDeviceData(boolean ignoreGap) {
+        deviceError.reset();
+        if (isLoggedIn(LOGIN_RETRIES)) {
+            if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
+                List<TapoRequest> requests = new ArrayList<>();
+                requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+                requests.add(new TapoRequest(DEVICE_CMD_GETENERGY));
+                connector.sendMultipleRequest(requests, ignoreGap);
+            } else {
+                connector.sendQueryCommand(DEVICE_CMD_GETINFO, ignoreGap);
+            }
+        }
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    public void newDataResult(String queryCommand) {
+        switch (queryCommand) {
+            case DEVICE_CMD_GETINFO:
+                baseDeviceData = connector.getResponseData(TapoBaseDeviceData.class);
+                updateBaseDeviceData();
+                break;
+            case DEVICE_CMD_GETENERGY:
+                energyData = connector.getResponseData(TapoEnergyData.class);
+                updateEnergyData(energyData);
+                break;
+            case DEVICE_CMD_CUSTOM:
+                responsePasstrough(connector.getResponseData(TapoResponse.class));
+                break;
+            default:
+                responsePasstrough(connector.getResponseData(TapoResponse.class));
+                break;
+        }
+        handleConnectionState();
+    }
+
+    /**
+     * handle baseDeviceData
+     */
+    private void updateBaseDeviceData() {
+        if (!baseDeviceData.getDeviceId().isBlank()) {
+            if (isExpectedThing(baseDeviceData)) {
+                updateDeviceProperties(baseDeviceData);
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "found type:'" + baseDeviceData.getModel() + "' with mac:'"
+                                + baseDeviceData.getRepresentationProperty() + "'. Check IP-Address");
+            }
+        } else {
+            logger.debug("({}) tried to update device with empty data", uid);
+        }
+    }
+
+    /**
+     * Handle full responsebody received from connector
+     * 
+     * @param fullResponse complete TapoResponse
+     */
+    public void responsePasstrough(TapoResponse fullResponse) {
+    }
+
+    /***********************************
+     *
+     * CONNECTION
+     *
+     ************************************/
+
+    /**
+     * Connect (login) to device
+     *
+     */
+    public boolean connect() {
+        deviceError.reset();
+        boolean loginSuccess = false;
+
+        try {
+            loginSuccess = connector.login();
+            if (loginSuccess) {
+                queryDeviceData(true);
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
+            }
+        } catch (Exception e) {
+            updateStatus(ThingStatus.UNKNOWN);
+        }
+        return loginSuccess;
+    }
+
+    /**
+     * disconnect device
+     */
+    public void disconnect() {
+        connector.logout();
+    }
+
+    /**
+     * check if is device is loged in.
+     * 
+     * @param totalRetries times to retry login
+     */
+    public boolean isLoggedIn(int totalRetries) {
+        if (connector.isLoggedIn()) {
+            return true;
+        } else {
+            logger.debug("({}) check if logged in but is not", uid);
+            for (int count = 0; count < totalRetries; count++) {
+                connect();
+            }
+            return false;
+        }
+    }
+
+    /**
+     * handle device state by connector error
+     */
+    public void handleConnectionState() {
+        ThingStatus deviceState = getThing().getStatus();
+        TapoErrorCode errorCode = deviceError.getError();
+
+        if (errorCode == TapoErrorCode.NO_ERROR) {
+            if (deviceState != ThingStatus.ONLINE) {
+                updateStatus(ThingStatus.ONLINE);
+            }
+        } else {
+            switch (errorCode.getType()) {
+                case COMMUNICATION_RETRY:
+                    connect();
+                    break;
+                case COMMUNICATION_ERROR:
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
+                    disconnect();
+                    break;
+                case CONFIGURATION_ERROR:
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
+                    break;
+                default:
+                    updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
+            }
+        }
+    }
+
+    /***********************************
+     * CHANNELS
+     ************************************/
+
+    /**
+     * Update Basic Device Properties
+     *
+     * If only one property must be changed, there is also a convenient method
+     * updateProperty(String name, String value).
+     *
+     * @param baseDeviceData Object BaseDeviceData
+     */
+    protected void updateDeviceProperties(TapoBaseDeviceData baseDeviceData) {
+        /* device properties */
+        Map<String, String> properties = editProperties();
+        properties.put(Thing.PROPERTY_MAC_ADDRESS, baseDeviceData.getMAC());
+        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, baseDeviceData.getFirmwareVersion());
+        properties.put(Thing.PROPERTY_HARDWARE_VERSION, baseDeviceData.getHardwareVersion());
+        properties.put(Thing.PROPERTY_MODEL_ID, baseDeviceData.getModel());
+        properties.put(Thing.PROPERTY_SERIAL_NUMBER, baseDeviceData.getDeviceId());
+        updateProperties(properties);
+    }
+
+    /**
+     * Update Energy Data Channels
+     */
+    protected void updateEnergyData(TapoEnergyData energyData) {
+        updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_POWER),
+                getPowerType(energyData.getCurrentPower(), Units.WATT));
+        updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_USAGE_TODAY),
+                getEnergyType(energyData.getTodayEnergy(), Units.WATT_HOUR));
+        updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_RUNTIME_TODAY),
+                getTimeType(energyData.getTodayRuntime(), Units.MINUTE));
+        updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_USAGE_MONTH),
+                getEnergyType(energyData.getMonthEnergy(), Units.WATT_HOUR));
+        updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_RUNTIME_MONTH),
+                getTimeType(energyData.getMonthRuntime(), Units.MINUTE));
+    }
+
+    /**
+     * Update custom channels of device
+     * 
+     * @param baseDeviceData extended devicebelonging dataclass
+     */
+    protected void updateChannels(final Class<? extends TapoBaseDeviceData> baseDeviceData) {
+    }
+
+    /**
+     * Get ChannelID including group
+     *
+     * @param group String channel-group
+     * @param channel String channel-name
+     * @return String channelID
+     */
+    protected String getChannelID(String group, String channel) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+        if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
+            return group + "#" + channel;
+        }
+        return channel;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoDeviceConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoDeviceConfiguration.java
new file mode 100644 (file)
index 0000000..bb96bd3
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolEnum;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoDeviceConfiguration {
+    /* THING CONFIGUTATION PROPERTYS */
+    public static final String CONFIG_DEVICE_IP = "ipAddress";
+    public static final String CONFIG_PROTOCOL = "protocol";
+    public static final String CONFIG_HTTP_PORT = "httpPort";
+    public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
+    public static final String CONFIG_BACKGROUND_DISCOVERY = "backgroundDiscovery";
+
+    /* thing configuration parameter. */
+    public String ipAddress = "";
+    public String protocol = "AES";
+    public int httpPort = 80;
+    public int pollingInterval = 30;
+    public boolean backgroundDiscovery = false;
+
+    /**
+     * Check for configuration errors
+     * 
+     * @return true if config is valid
+     * @throws TapoErrorHandler
+     */
+    public boolean checkConfig() throws TapoErrorHandler {
+        if (!ipAddress.matches(IPV4_REGEX)) {
+            throw new TapoErrorHandler(ERR_CONFIG_IP);
+        }
+        try {
+            TapoProtocolEnum.valueOfString(protocol);
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_CONFIG_PROTOCOL);
+        }
+        return true;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoUniversalDeviceHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoUniversalDeviceHandler.java
new file mode 100644 (file)
index 0000000..262f2f3
--- /dev/null
@@ -0,0 +1,287 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.lightstrip.TapoLightStripData;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Universal-Device
+ * universal device for testing pruposes
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoUniversalDeviceHandler extends TapoBaseDeviceHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoUniversalDeviceHandler.class);
+    private TapoLightStripData deviceData = new TapoLightStripData();
+    private TapoRequest manualRequest = new TapoRequest(DEVICE_CMD_GETINFO);
+    private boolean useSecurePassthrough = true;
+
+    // Channel List for "Test- and Debug-Device"
+    public static final String CHANNEL_GROUP_RESPONSE = "response";
+    public static final String CHANNEL_GROUP_COMMAND = "devicecommand";
+    public static final String CHANNEL_RESPONSE = "deviceResponse";
+    public static final String CHANNEL_COMMAND_METHOD = "method";
+    public static final String CHANNEL_COMMAND_PARAMS = "params";
+    public static final String CHANNEL_COMMAND_SECURE = "secure";
+    public static final String CHANNEL_COMMAND_SEND = "sendCommand";
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    public TapoUniversalDeviceHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    @Override
+    public void newDataResult(String queryCommand) {
+        super.newDataResult(queryCommand);
+        if (DEVICE_CMD_GETINFO.equals(queryCommand)) {
+            deviceData = connector.getResponseData(TapoLightStripData.class);
+            updateChannels(deviceData);
+        }
+    }
+
+    /**
+     * query device Properties
+     */
+    @Override
+    public void queryDeviceData() {
+        deviceError.reset();
+        if (isLoggedIn(LOGIN_RETRIES)) {
+            connector.sendQueryCommand(DEVICE_CMD_GETINFO, true);
+            connector.sendQueryCommand(DEVICE_CMD_GETENERGY, true);
+        }
+    }
+
+    /*****************************
+     * HANDLE COMMANDS
+     *****************************/
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command, channelUID.getId());
+
+        String group = channelUID.getGroupId();
+        if (command instanceof RefreshType) {
+            queryDeviceData();
+        } else {
+            if (CHANNEL_GROUP_RESPONSE.equals(group) || CHANNEL_GROUP_COMMAND.equals(group)) {
+                handleSpecialCommands(channelUID, command);
+            } else {
+                handleStandardCommands(channelUID, command);
+            }
+        }
+    }
+
+    /**
+     * Handle standard commands for debug-, test-devices
+     */
+    private void handleStandardCommands(ChannelUID channelUID, Command command) {
+        String channel = channelUID.getIdWithoutGroup();
+
+        switch (channel) {
+            case CHANNEL_OUTPUT:
+                handleOnOffCommand(command);
+                break;
+            case CHANNEL_BRIGHTNESS:
+                handleBrightnessCommand(command);
+                break;
+            case CHANNEL_COLOR_TEMP:
+                handleColorTempCommand(command);
+                break;
+            case CHANNEL_COLOR:
+                handleColorCommand(command);
+                break;
+            default:
+                logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+        }
+    }
+
+    /**
+     * Handle special commands for debug-, test-devices
+     */
+    private void handleSpecialCommands(ChannelUID channelUID, Command command) {
+        String channel = channelUID.getIdWithoutGroup();
+
+        if (CHANNEL_GROUP_COMMAND.equals(channelUID.getGroupId())) {
+            switch (channel) {
+                case CHANNEL_COMMAND_METHOD:
+                    manualRequest = new TapoRequest(command.toString(), manualRequest.params());
+                    break;
+                case CHANNEL_COMMAND_PARAMS:
+                    manualRequest = new TapoRequest(manualRequest.method(), command.toString());
+                    break;
+                case CHANNEL_COMMAND_SECURE:
+                    useSecurePassthrough = !useSecurePassthrough;
+                    break;
+                case CHANNEL_COMMAND_SEND:
+                    /* send manual request */
+                    if (useSecurePassthrough) {
+                        logger.debug("({}) sendSecurePasstrough '{}' ", uid, manualRequest);
+                        connector.sendAsyncRequest(manualRequest);
+                    } else {
+                        logger.debug("({}) sendRawCommand '{}' ", uid, manualRequest);
+                        connector.sendRawCommand(manualRequest);
+                    }
+                    break;
+                default:
+                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command,
+                            channelUID.getId());
+            }
+
+        } else {
+            if (CHANNEL_RESPONSE.equals(channel)) {
+                logger.debug("({}) NOT IMPLEMENTED COMMAND {} ", uid, command);
+            }
+        }
+    }
+
+    private void handleOnOffCommand(Command command) {
+        switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+    }
+
+    private void handleBrightnessCommand(Command command) {
+        if (command instanceof PercentType percentCommand) {
+            Float percent = percentCommand.floatValue();
+            setBrightness(percent.intValue()); // 0..100% = 0..100
+        } else if (command instanceof DecimalType decimalCommand) {
+            setBrightness(decimalCommand.intValue());
+        }
+    }
+
+    private void handleColorCommand(Command command) {
+        if (command instanceof HSBType hsbCommand) {
+            setColor(hsbCommand);
+        }
+    }
+
+    private void handleColorTempCommand(Command command) {
+        if (command instanceof DecimalType decimalCommand) {
+            setColorTemp(decimalCommand.intValue());
+        }
+    }
+
+    /*****************************
+     * SEND COMMANDS
+     *****************************/
+
+    /**
+     * Switch device On or Off
+     * 
+     * @param on if true device will switch on. Otherwise switch off
+     */
+    protected void switchOnOff(boolean on) {
+        deviceData.switchOnOff(on);
+        connector.sendCommandAndQuery(deviceData, false);
+    }
+
+    /**
+     * SET BRIGHTNESS
+     * 
+     * @param newBrightness percentage 0-100 of new brightness
+     */
+    protected void setBrightness(Integer newBrightness) {
+        /* switch off if 0 */
+        if (newBrightness == 0) {
+            deviceData.switchOff();
+        } else {
+            deviceData.switchOn();
+            deviceData.setBrightness(newBrightness);
+            connector.sendCommandAndQuery(deviceData, false);
+        }
+    }
+
+    /**
+     * SET COLOR
+     * 
+     * @param command HSBType
+     */
+    protected void setColor(HSBType command) {
+        deviceData.switchOn();
+        deviceData.setHue(command.getHue().intValue());
+        deviceData.setSaturation(command.getSaturation().intValue());
+        deviceData.setBrightness(command.getBrightness().intValue());
+        connector.sendCommandAndQuery(deviceData, false);
+    }
+
+    /**
+     * SET COLORTEMP
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        deviceData.switchOn();
+        deviceData.setColorTemp(colorTemp);
+        connector.sendCommandAndQuery(deviceData, false);
+    }
+
+    /**
+     * Handle full responsebody received from connector
+     * 
+     * @param fullResponse TapoResponse received
+     */
+    @Override
+    public void responsePasstrough(TapoResponse fullResponse) {
+        String response = fullResponse.result().getAsString();
+        logger.debug("({}) received response {}", uid, response);
+        updateState(getChannelID(CHANNEL_GROUP_RESPONSE, CHANNEL_RESPONSE), getStringType(response));
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * @param deviceInfo TapoLightStripData
+     */
+    protected void updateChannels(TapoLightStripData deviceInfo) {
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceInfo.getBrightness()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+                getDecimalType(deviceInfo.getColorTemp()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
+
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceInfo.getSignalLevel()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getTimeType(deviceInfo.getOnTime(), Units.SECOND));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
+                getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbData.java
new file mode 100644 (file)
index 0000000..2b611e0
--- /dev/null
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.bulb;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBulbData extends TapoBaseDeviceData {
+    @SerializedName("device_on")
+    @Expose(serialize = true, deserialize = true)
+    private boolean deviceOn = false;
+
+    @Expose(serialize = true, deserialize = true)
+    private int brightness = 100;
+
+    @SerializedName("color_temp")
+    @Expose(serialize = true, deserialize = true)
+    private int colorTemp = 0;
+
+    @Expose(serialize = true, deserialize = true)
+    private int hue = 0;
+
+    @Expose(serialize = true, deserialize = true)
+    private int saturation = 100;
+
+    @Expose(serialize = false, deserialize = true)
+    private long onTime = 0;
+
+    @SerializedName("time_usage_past7")
+    @Expose(serialize = false, deserialize = true)
+    private long timeUsagePast7 = 0;
+
+    @SerializedName("time_usage_past30")
+    @Expose(serialize = false, deserialize = true)
+    private long timeUsagePast30 = 0;
+
+    @SerializedName("time_usage_today")
+    @Expose(serialize = false, deserialize = true)
+    private long timeUsageToday = 0;
+
+    @SerializedName("dynamic_light_effect_enable")
+    @Expose(serialize = false, deserialize = true)
+    private boolean dynamicLightEffectEnable = false;
+
+    @SerializedName("dynamic_light_effect_id")
+    @Expose(serialize = false, deserialize = true)
+    private String dynamicLightEffectId = "";
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    public void switchOn() {
+        deviceOn = true;
+    }
+
+    public void switchOff() {
+        deviceOn = false;
+    }
+
+    public void switchOnOff(boolean on) {
+        deviceOn = on;
+    }
+
+    public void setBrightness(int value) {
+        brightness = value;
+    }
+
+    public void setColorTemp(int value) {
+        colorTemp = value;
+    }
+
+    public void setHue(int value) {
+        hue = value;
+    }
+
+    public void setSaturation(int value) {
+        saturation = value;
+    }
+
+    public void setDynamicLightEffectId(String fxId) {
+        dynamicLightEffectId = fxId;
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+    public boolean dynamicLightEffectEnabled() {
+        return dynamicLightEffectEnable;
+    }
+
+    public String getDynamicLightEffectId() {
+        return dynamicLightEffectId;
+    }
+
+    public int getBrightness() {
+        return brightness;
+    }
+
+    public int getColorTemp() {
+        return colorTemp;
+    }
+
+    public HSBType getHSB() {
+        DecimalType h = new DecimalType(hue);
+        PercentType s = new PercentType(saturation);
+        PercentType b = new PercentType(brightness);
+        return new HSBType(h, s, b);
+    }
+
+    public int getHue() {
+        return hue;
+    }
+
+    public boolean isOff() {
+        return !deviceOn;
+    }
+
+    public boolean isOn() {
+        return deviceOn;
+    }
+
+    public long getOnTime() {
+        return onTime;
+    }
+
+    public int getSaturation() {
+        return saturation;
+    }
+
+    public long getTimeUsagePast7() {
+        return timeUsagePast7;
+    }
+
+    public long getTimeUsagePast30() {
+        return timeUsagePast30;
+    }
+
+    public long getTimeUsagePastToday() {
+        return timeUsageToday;
+    }
+
+    public TapoBulbModeEnum getWorkingMode() {
+        if (dynamicLightEffectEnable) {
+            return TapoBulbModeEnum.LIGHT_FX;
+        } else if (colorTemp == 0) {
+            return TapoBulbModeEnum.COLOR_LIGHT;
+        } else {
+            return TapoBulbModeEnum.WHITE_LIGHT;
+        }
+    }
+
+    public boolean supportsMultiRequest() {
+        return !getHardwareVersion().startsWith("1");
+    }
+
+    @Override
+    public String toString() {
+        return toJson();
+    }
+
+    public String toJson() {
+        return GSON.toJson(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbHandler.java
new file mode 100644 (file)
index 0000000..9f1194e
--- /dev/null
@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.bulb;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoLightDynamicFx;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBulbHandler extends TapoBaseDeviceHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoBulbHandler.class);
+    private TapoBulbData bulbData = new TapoBulbData();
+    private TapoBulbLastStates lastStates = new TapoBulbLastStates();
+
+    /**
+     * Constructor
+     * 
+     * @param thing Thing object representing device
+     */
+    public TapoBulbHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    @Override
+    public void newDataResult(String queryCommand) {
+        super.newDataResult(queryCommand);
+        if (DEVICE_CMD_GETINFO.equals(queryCommand)) {
+            bulbData = connector.getResponseData(TapoBulbData.class);
+            lastStates.put(bulbData.getWorkingMode(), bulbData);
+            updateChannels(bulbData);
+        }
+    }
+
+    /*****************************
+     * HANDLE COMMANDS
+     *****************************/
+
+    /**
+     * handle command sent to device
+     * 
+     * @param channelUID channelUID command is sent to
+     * @param command command to be sent
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        String channel = channelUID.getIdWithoutGroup();
+        if (command instanceof RefreshType) {
+            queryDeviceData();
+        } else {
+            switch (channel) {
+                case CHANNEL_OUTPUT:
+                    handleOnOffCommand(command);
+                    break;
+                case CHANNEL_BRIGHTNESS:
+                    handleBrightnessCommand(command);
+                    break;
+                case CHANNEL_COLOR_TEMP:
+                    handleColorTempCommand(command);
+                    break;
+                case CHANNEL_COLOR:
+                    handleColorCommand(command);
+                    break;
+                case CHANNEL_FX_NAME:
+                    handleLightFx(command);
+                    break;
+                case CHANNEL_MODE:
+                    handleModeChange(command);
+                    break;
+                default:
+                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command,
+                            channelUID.getId());
+            }
+        }
+    }
+
+    private void handleOnOffCommand(Command command) {
+        switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+    }
+
+    private void handleBrightnessCommand(Command command) {
+        if (command instanceof PercentType percentCommand) {
+            Float percent = percentCommand.floatValue();
+            setBrightness(percent.intValue()); // 0..100% = 0..100
+        } else if (command instanceof DecimalType decimalCommand) {
+            setBrightness(decimalCommand.intValue());
+        }
+    }
+
+    private void handleColorCommand(Command command) {
+        if (command instanceof HSBType hsbCommand) {
+            setColor(hsbCommand);
+        }
+    }
+
+    private void handleColorTempCommand(Command command) {
+        if (command instanceof DecimalType decimalCommand) {
+            setColorTemp(decimalCommand.intValue());
+        }
+    }
+
+    private void handleLightFx(Command command) {
+        setLightEffect(command.toString());
+    }
+
+    private void handleModeChange(Command command) {
+        setLastMode(TapoBulbModeEnum.valueOf(command.toString()));
+    }
+
+    /*****************************
+     * SEND COMMANDS
+     *****************************/
+
+    /**
+     * Switch device On or Off
+     * 
+     * @param on if true device will switch on. Otherwise switch off
+     */
+    protected void switchOnOff(boolean on) {
+        bulbData.switchOnOff(on);
+        connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+    }
+
+    /**
+     * Set Britghtness of device
+     * 
+     * @param newBrightness percentage 0-100 of new brightness
+     */
+    protected void setBrightness(Integer newBrightness) {
+        /* switch off if 0 */
+        if (newBrightness == 0) {
+            bulbData.switchOff();
+        } else {
+            bulbData.switchOn();
+            bulbData.setBrightness(newBrightness);
+        }
+        connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+    }
+
+    /**
+     * Set Color of Device
+     * 
+     * @param command HSBType
+     */
+    protected void setColor(HSBType command) {
+        bulbData.switchOn();
+        bulbData.setColorTemp(0);
+        bulbData.setHue(command.getHue().intValue());
+        bulbData.setSaturation(command.getSaturation().intValue());
+        bulbData.setBrightness(command.getBrightness().intValue());
+        connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+    }
+
+    /**
+     * Set ColorTemp
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        bulbData.switchOn();
+        bulbData.setHue(0);
+        bulbData.setColorTemp(colorTemp);
+        connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+    }
+
+    /**
+     * Set light effect
+     * 
+     * @param fxId (String) id of LightEffect
+     */
+    protected void setLightEffect(String fxId) {
+        try {
+            TapoLightDynamicFx fxData = new TapoLightDynamicFx();
+            fxData.setEffect(fxId);
+            connector.sendCommandAndQuery(DEVICE_CMD_SET_DYNAIMCLIGHT_FX, fxData, bulbData.supportsMultiRequest());
+        } catch (TapoErrorHandler te) {
+            logger.warn("({}) could not load effect '{}' - {}", uid, fxId, te.getMessage());
+        }
+    }
+
+    /**
+     * Set last state by mode
+     * 
+     * @param mode mode to set
+     */
+    protected void setLastMode(TapoBulbModeEnum mode) {
+        TapoBulbData lastState = lastStates.get(mode);
+        if (TapoBulbModeEnum.LIGHT_FX.equals(mode)) {
+            setLightEffect(lastState.getDynamicLightEffectId());
+        } else {
+            connector.sendCommandAndQuery(lastState, bulbData.supportsMultiRequest());
+        }
+    }
+
+    /*****************************
+     * UPDATE CHANNELS
+     *****************************/
+
+    protected void updateChannels(TapoBulbData deviceData) {
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceData.isOn()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_MODE),
+                getStringType(deviceData.getWorkingMode().toString()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceData.getBrightness()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+                getDecimalType(deviceData.getColorTemp()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceData.getHSB());
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceData.getSignalLevel()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getTimeType(deviceData.getOnTime(), Units.SECOND));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceData.isOverheated()));
+
+        updateLightEffectChannels(deviceData);
+    }
+
+    /**
+     * Update light effect channels
+     */
+    protected void updateLightEffectChannels(TapoBulbData deviceData) {
+        String fxId = "";
+        if (deviceData.dynamicLightEffectEnabled()) {
+            fxId = deviceData.getDynamicLightEffectId();
+        }
+        updateState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(fxId));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbLastStates.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbLastStates.java
new file mode 100644 (file)
index 0000000..498417e
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.bulb;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+
+import java.util.EnumMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * List with Tapo-Bulb Information LastState
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBulbLastStates {
+    private EnumMap<TapoBulbModeEnum, @Nullable TapoBulbData> lastModeData = new EnumMap<>(TapoBulbModeEnum.class);
+
+    /**
+     * INIT
+     */
+    public TapoBulbLastStates() {
+        lastModeData.put(TapoBulbModeEnum.UNKOWN, new TapoBulbData());
+    }
+
+    /**
+     * Return LastModeData of Mode
+     * 
+     * @param mode mode to get
+     * @return
+     */
+    public TapoBulbData get(TapoBulbModeEnum mode) {
+        if (lastModeData.containsKey(mode)) {
+            TapoBulbData lastmode = lastModeData.get(mode);
+            if (lastmode != null) {
+                return lastmode;
+            }
+        }
+        return getDefaultData(mode);
+    }
+
+    /**
+     * Set LastmodeData for Mode
+     * 
+     * @param mode mode to set
+     * @param data actual state-data
+     */
+    public void put(TapoBulbModeEnum mode, TapoBulbData data) {
+        lastModeData.put(mode, data);
+    }
+
+    /**
+     * Get DefaultData
+     * 
+     * @param mode mode to get
+     * @return
+     */
+    private TapoBulbData getDefaultData(TapoBulbModeEnum mode) {
+        TapoBulbData defaultData = new TapoBulbData();
+        defaultData.setBrightness(100);
+        defaultData.setSaturation(100);
+        defaultData.switchOnOff(true);
+        defaultData.setDynamicLightEffectId("L1");
+
+        if (TapoBulbModeEnum.WHITE_LIGHT.equals(mode)) {
+            defaultData.setColorTemp(BULB_MAX_COLORTEMP - BULB_MIN_COLORTEMP);
+        }
+        return defaultData;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbModeEnum.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbModeEnum.java
new file mode 100644 (file)
index 0000000..0090a2c
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.bulb;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Tapo-Bulb-Mode Enum
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public enum TapoBulbModeEnum {
+    UNKOWN,
+    WHITE_LIGHT,
+    COLOR_LIGHT,
+    LIGHT_FX;
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubData.java
new file mode 100644 (file)
index 0000000..8f089bf
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.hub;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Hub Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoHubData extends TapoBaseDeviceData {
+    @SerializedName("in_alarm")
+    @Expose(serialize = false, deserialize = true)
+    private boolean alarmActive = false;
+
+    @SerializedName("in_alarm_source")
+    @Expose(serialize = false, deserialize = true)
+    private String alarmSource = "";
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+    public boolean alarmIsActive() {
+        return alarmActive;
+    }
+
+    public String getAlarmSource() {
+        return alarmSource;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubHandler.java
new file mode 100644 (file)
index 0000000..7575d58
--- /dev/null
@@ -0,0 +1,288 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.hub;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildList;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.discovery.TapoChildDiscoveryService;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoHubHandler} is responsible for handling commands, which are
+ * sent to the child devices of the hub with a bridge.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoHubHandler extends TapoBaseDeviceHandler implements BridgeHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoHubHandler.class);
+    protected TapoHubData hubData = new TapoHubData();
+    private TapoChildList tapoChildsList = new TapoChildList();
+    private @NonNullByDefault({}) TapoChildDiscoveryService discoveryService;
+    private List<Thing> tapoChildThings = new ArrayList<>();
+
+    public TapoHubHandler(Thing thing) {
+        super(thing);
+        logger.trace("{} Hub initialized", uid);
+    }
+
+    /**
+     * Activate Device
+     */
+    @Override
+    protected void activateDevice() {
+        super.activateDevice();
+        discoveryService.setBackGroundDiscovery(deviceConfig.backgroundDiscovery);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("{} Hub doesn't handle command: {}", uid, command);
+    }
+
+    @Override
+    public void dispose() {
+        logger.trace("{} Hub disposed ", uid);
+        super.dispose();
+    }
+
+    @Override
+    public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
+        logger.trace("({}) childHandlerInitialized '{}'", uid, childThing.getUID());
+        tapoChildThings.add(childThing);
+    }
+
+    @Override
+    public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
+        logger.trace("({}) childHandlerDisposed '{}'", uid, childThing.getUID());
+        tapoChildThings.remove(childThing);
+    }
+
+    /***********************************
+     *
+     * CHILD DISCOVERY SERVICE
+     *
+     ************************************/
+
+    /**
+     * ACTIVATE DISCOVERY SERVICE
+     */
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Set.of(TapoChildDiscoveryService.class);
+    }
+
+    /**
+     * Set DiscoveryService
+     * 
+     * @param discoveryService
+     */
+    public void setDiscoveryService(TapoChildDiscoveryService discoveryService) {
+        discoveryService.setBackGroundDiscovery(deviceConfig.backgroundDiscovery);
+        this.discoveryService = discoveryService;
+    }
+
+    /****************************
+     * PUBLIC FUNCTIONS
+     ****************************/
+
+    /**
+     * query device Properties
+     */
+    @Override
+    public void queryDeviceData() {
+        deviceError.reset();
+        if (isLoggedIn(LOGIN_RETRIES)) {
+            List<TapoRequest> requests = new ArrayList<>();
+            requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+            requests.add(new TapoRequest(DEVICE_CMD_GETCHILDDEVICELIST));
+            connector.sendMultipleRequest(requests);
+        }
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    @Override
+    public void newDataResult(String queryCommand) {
+        super.newDataResult(queryCommand);
+        switch (queryCommand) {
+            case DEVICE_CMD_GETINFO:
+                hubData = connector.getResponseData(TapoHubData.class);
+                updateChannels(hubData);
+                break;
+            case DEVICE_CMD_GETCHILDDEVICELIST:
+                tapoChildsList = connector.getResponseData(TapoChildList.class);
+                updateChildDevices(tapoChildsList);
+                break;
+            default:
+                responsePasstrough(connector.getResponseData(TapoResponse.class));
+                break;
+        }
+    }
+
+    /****************************
+     * CHILD THINGS
+     ****************************/
+
+    /**
+     * Update all Child-Things
+     */
+    public void updateChildThings() {
+        for (Thing thing : tapoChildThings) {
+            updateChild(thing);
+        }
+    }
+
+    /**
+     * Update Child single child with special representationProperty
+     *
+     * @param thingTypeToUpdate ThingTypeUID of Thing to update
+     * @param representationProperty Name of representationProperty
+     * @param propertyValue Value of representationProperty
+     */
+    public void updateChild(ThingTypeUID thingTypeToUpdate, String representationProperty, String propertyValue) {
+        for (Thing thing : tapoChildThings) {
+            ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+            if (thingTypeToUpdate.equals(thingTypeUID)) {
+                String thingProperty = thing.getProperties().get(representationProperty);
+                if (propertyValue.equals(thingProperty)) {
+                    updateChild(thing);
+                }
+            }
+        }
+    }
+
+    /**
+     * Update Child-Thing (send refreshCommand)
+     *
+     * @param thing - Thing to update
+     */
+    public void updateChild(Thing thing) {
+        ThingHandler handler = thing.getHandler();
+        if (handler != null) {
+            ChannelUID cUid = new ChannelUID(thing.getUID(), "any");
+            handler.handleCommand(cUid, RefreshType.REFRESH);
+        }
+    }
+
+    /**
+     * Set State of all clients
+     *
+     * @param thingStatus new ThingStatus
+     */
+    public void updateChildStates(ThingStatus thingStatus) {
+        for (Thing thing : tapoChildThings) {
+            updateChildState(thing, thingStatus);
+        }
+    }
+
+    /**
+     * Set State of a Thing
+     *
+     * @param thing Thing to update
+     * @param thingStatus new ThingStatus
+     */
+    public void updateChildState(Thing thing, ThingStatus thingStatus) {
+        logger.trace("{} set child states to {} by hub", uid, thingStatus);
+        ThingHandler handler = thing.getHandler();
+        if (handler != null) {
+            if (ThingStatus.OFFLINE.equals(thingStatus)) {
+                handler.bridgeStatusChanged(new ThingStatusInfo(thingStatus, ThingStatusDetail.BRIDGE_OFFLINE, ""));
+            } else {
+                handler.bridgeStatusChanged(new ThingStatusInfo(thingStatus, ThingStatusDetail.NONE, ""));
+            }
+        }
+    }
+
+    /****************************
+     * UPDATE HUB CHANNELS
+     ****************************/
+
+    /**
+     * Update Channels
+     */
+    protected void updateChannels(TapoHubData hubData) {
+        updateState(getChannelID(CHANNEL_GROUP_ALARM, CHANNEL_ALARM_ACTIVE), getOnOffType(hubData.alarmIsActive()));
+        updateState(getChannelID(CHANNEL_GROUP_ALARM, CHANNEL_ALARM_SOURCE), getStringType(hubData.getAlarmSource()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(hubData.getSignalLevel()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(hubData.isOverheated()));
+    }
+
+    /**
+     * Update channels of childdevices
+     */
+    protected void updateChildDevices(TapoChildList childList) {
+        tapoChildsList = childList;
+        if (discoveryService.isBackgroundDiscoveryEnabled()) {
+            discoveryService.thingsDiscovered(childList.getChildDeviceList());
+        }
+        /* update children */
+        updateChildThings();
+    }
+
+    /****************************
+     * HUB GETTERS
+     ****************************/
+
+    public ThingUID getUID() {
+        return getThing().getUID();
+    }
+
+    public List<TapoChildDeviceData> getChildDevices() {
+        return tapoChildsList.getChildDeviceList();
+    }
+
+    public TapoChildDeviceData getChild(String deviceSerial) {
+        List<TapoChildDeviceData> childDeviceList = tapoChildsList.getChildDeviceList();
+        for (int i = 0; i <= childDeviceList.size(); i++) {
+            TapoChildDeviceData child = childDeviceList.get(i);
+            if (child.getDeviceId().equals(deviceSerial)) {
+                return child;
+            }
+        }
+        logger.debug("child not found in deviceList '{}'", deviceSerial);
+        return new TapoChildDeviceData();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripData.java
new file mode 100644 (file)
index 0000000..313bcf3
--- /dev/null
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.lightstrip;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoLightEffect;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightStripData extends TapoBaseDeviceData {
+    @SerializedName("device_on")
+    @Expose(serialize = true, deserialize = true)
+    private boolean deviceOn = false;
+
+    @Expose(serialize = true, deserialize = true)
+    private int brightness = 0;
+
+    @SerializedName("color_temp")
+    @Expose(serialize = true, deserialize = true)
+    private int colorTemp = 0;
+
+    @Expose(serialize = true, deserialize = true)
+    private int hue = 0;
+
+    @Expose(serialize = true, deserialize = true)
+    private int saturation = 100;
+
+    @SerializedName("on_time")
+    @Expose(serialize = false, deserialize = true)
+    private long onTime = 0;
+
+    @SerializedName("music_rhythm_enable")
+    @Expose(serialize = true, deserialize = true)
+    private boolean musicRythmEnable = false;
+
+    @SerializedName("music_rhythm_mode")
+    @Expose(serialize = true, deserialize = true)
+    private String musicRythmMode = "";
+
+    @SerializedName("lighting_effect")
+    @Expose(serialize = false, deserialize = true)
+    private TapoLightEffect lightingEffect = new TapoLightEffect();
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+    public void switchOn() {
+        deviceOn = true;
+    }
+
+    public void switchOff() {
+        deviceOn = false;
+    }
+
+    public void switchOnOff(boolean on) {
+        deviceOn = on;
+    }
+
+    public void setBrightness(int value) {
+        if (lightingEffect.isEnabled()) {
+            lightingEffect.setBrightness(value);
+        } else {
+            brightness = value;
+        }
+    }
+
+    public void setColorTemp(int value) {
+        colorTemp = value;
+    }
+
+    public void setHue(int value) {
+        hue = value;
+    }
+
+    public void setSaturation(int value) {
+        saturation = value;
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public int getBrightness() {
+        if (lightingEffect.isEnabled()) {
+            return lightingEffect.getBrightness();
+        } else {
+            return brightness;
+        }
+    }
+
+    public int getColorTemp() {
+        return colorTemp;
+    }
+
+    public HSBType getHSB() {
+        DecimalType h = new DecimalType(hue);
+        PercentType s = new PercentType(saturation);
+        PercentType b = new PercentType(brightness);
+        return new HSBType(h, s, b);
+    }
+
+    public int getHue() {
+        return hue;
+    }
+
+    public TapoLightEffect getLightEffect() {
+        return lightingEffect;
+    }
+
+    public boolean isOff() {
+        return !deviceOn;
+    }
+
+    public boolean isOn() {
+        return deviceOn;
+    }
+
+    public int getSaturation() {
+        return saturation;
+    }
+
+    public Number getOnTime() {
+        return onTime;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripHandler.java
new file mode 100644 (file)
index 0000000..4e9c721
--- /dev/null
@@ -0,0 +1,253 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.lightstrip;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoLightEffect;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightStripHandler extends TapoBaseDeviceHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoLightStripHandler.class);
+    private TapoLightStripData lightStripData = new TapoLightStripData();
+
+    /**
+     * Constructor
+     * 
+     * @param thing Thing object representing device
+     */
+    public TapoLightStripHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    @Override
+    public void newDataResult(String queryCommand) {
+        super.newDataResult(queryCommand);
+        if (DEVICE_CMD_GETINFO.equals(queryCommand)) {
+            lightStripData = connector.getResponseData(TapoLightStripData.class);
+            updateChannels(lightStripData);
+        }
+    }
+
+    /*****************************
+     * HANDLE COMMANDS
+     *****************************/
+
+    /**
+     * handle command sent to device
+     * 
+     * @param channelUID channelUID command is sent to
+     * @param command command to be sent
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        String channel = channelUID.getIdWithoutGroup();
+        String group = channelUID.getGroupId();
+        if (command instanceof RefreshType) {
+            queryDeviceData();
+        } else if (CHANNEL_GROUP_EFFECTS.equals(group)) {
+            handleLightFx(channel, command);
+        } else {
+            switch (channel) {
+                case CHANNEL_OUTPUT:
+                    handleOnOffCommand(command);
+                    break;
+                case CHANNEL_BRIGHTNESS:
+                    if (lightStripData.getLightEffect().isEnabled()) {
+                        handleLightFx(channel, command);
+                    } else {
+                        handleBrightnessCommand(command);
+                    }
+                    break;
+                case CHANNEL_COLOR_TEMP:
+                    handleColorTempCommand(command);
+                    break;
+                case CHANNEL_COLOR:
+                    handleColorCommand(command);
+                    break;
+                default:
+                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command,
+                            channelUID.getId());
+            }
+        }
+    }
+
+    private void handleOnOffCommand(Command command) {
+        switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+    }
+
+    private void handleBrightnessCommand(Command command) {
+        if (command instanceof PercentType percentCommand) {
+            Float percent = percentCommand.floatValue();
+            setBrightness(percent.intValue()); // 0..100% = 0..100
+        } else if (command instanceof DecimalType decimalCommand) {
+            setBrightness(decimalCommand.intValue());
+        }
+    }
+
+    private void handleColorCommand(Command command) {
+        if (command instanceof HSBType hsbCommand) {
+            setColor(hsbCommand);
+        }
+    }
+
+    private void handleColorTempCommand(Command command) {
+        if (command instanceof DecimalType decimalCommand) {
+            setColorTemp(decimalCommand.intValue());
+        }
+    }
+
+    private void handleLightFx(String channel, Command command) {
+        TapoLightEffect lightEffect = lightStripData.getLightEffect();
+        switch (channel) {
+            case CHANNEL_BRIGHTNESS:
+                if (command instanceof PercentType percentCommand) {
+                    Float percent = percentCommand.floatValue();
+                    lightEffect.setBrightness(percent.intValue()); // 0..100% = 0..100
+                } else if (command instanceof DecimalType decimalCommand) {
+                    lightEffect.setBrightness(decimalCommand.intValue());
+                }
+                break;
+            case CHANNEL_FX_NAME:
+                try {
+                    lightEffect = lightEffect.setEffect(command.toString());
+                } catch (Exception e) {
+                    logger.warn("({}) could not load effect '{}'", uid, command);
+                }
+
+                break;
+            default:
+                logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channel);
+        }
+        setLightEffect(lightEffect);
+    }
+
+    /*****************************
+     * SEND COMMANDS
+     *****************************/
+
+    /**
+     * Switch device On or Off
+     * 
+     * @param on if true device will switch on. Otherwise switch off
+     */
+    protected void switchOnOff(boolean on) {
+        lightStripData.switchOnOff(on);
+        connector.sendCommandAndQuery(lightStripData, true);
+    }
+
+    /**
+     * Set Britghtness of device
+     * 
+     * @param newBrightness percentage 0-100 of new brightness
+     */
+    protected void setBrightness(Integer newBrightness) {
+        /* switch off if 0 */
+        if (newBrightness == 0) {
+            lightStripData.switchOff();
+        } else {
+            lightStripData.switchOn();
+            lightStripData.setBrightness(newBrightness);
+        }
+        connector.sendCommandAndQuery(lightStripData, true);
+    }
+
+    /**
+     * Set Color of Device
+     * 
+     * @param command HSBType
+     */
+    protected void setColor(HSBType command) {
+        lightStripData.switchOn();
+        lightStripData.setHue(command.getHue().intValue());
+        lightStripData.setSaturation(command.getSaturation().intValue());
+        lightStripData.setBrightness(command.getBrightness().intValue());
+        connector.sendCommandAndQuery(lightStripData, true);
+    }
+
+    /**
+     * Set ColorTemp
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        lightStripData.switchOn();
+        lightStripData.setColorTemp(colorTemp);
+        connector.sendCommandAndQuery(lightStripData, true);
+    }
+
+    /**
+     * Set light effect
+     * 
+     * @param lightEffect TapoLightEffect
+     */
+    protected void setLightEffect(TapoLightEffect lightEffect) {
+        connector.sendDeviceCommand(DEVICE_CMD_SET_LIGHT_FX, lightEffect);
+    }
+
+    /*****************************
+     * UPDATE CHANNELS
+     *****************************/
+
+    protected void updateChannels(TapoLightStripData deviceData) {
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceData.isOn()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceData.getBrightness()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+                getDecimalType(deviceData.getColorTemp()));
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceData.getHSB());
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceData.getSignalLevel()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getTimeType(deviceData.getOnTime(), Units.SECOND));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceData.isOverheated()));
+
+        updateLightEffectChannels(deviceData);
+    }
+
+    /**
+     * Update light effect channels
+     */
+    protected void updateLightEffectChannels(TapoLightStripData deviceData) {
+        TapoLightEffect lightEffect = deviceData.getLightEffect();
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceData.getBrightness()));
+        updateState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(lightEffect.getName()));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketData.java
new file mode 100644 (file)
index 0000000..600235e
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.socket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSocketData extends TapoBaseDeviceData {
+    @SerializedName("device_on")
+    @Expose(serialize = true, deserialize = true)
+    private boolean deviceOn = false;
+
+    @SerializedName("on_time")
+    @Expose(serialize = false, deserialize = true)
+    private long onTime = 0;
+
+    @SerializedName("time_usage_past7")
+    @Expose(serialize = false, deserialize = true)
+    private long timeUsagePast7 = 0;
+
+    @SerializedName("time_usage_past30")
+    @Expose(serialize = false, deserialize = true)
+    private long timeUsagePast30 = 0;
+
+    @SerializedName("time_usage_today")
+    @Expose(serialize = false, deserialize = true)
+    private long timeUsageToday = 0;
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    public void switchOn() {
+        deviceOn = true;
+    }
+
+    public void switchOff() {
+        deviceOn = false;
+    }
+
+    public void switchOnOff(boolean on) {
+        deviceOn = on;
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public boolean isOff() {
+        return !deviceOn;
+    }
+
+    public boolean isOn() {
+        return deviceOn;
+    }
+
+    public Number getOnTime() {
+        return onTime;
+    }
+
+    public long getTimeUsagePast7() {
+        return timeUsagePast7;
+    }
+
+    public long getTimeUsagePast30() {
+        return timeUsagePast30;
+    }
+
+    public long getTimeUsagePastToday() {
+        return timeUsageToday;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketHandler.java
new file mode 100644 (file)
index 0000000..7c641df
--- /dev/null
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.socket;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSocketHandler extends TapoBaseDeviceHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoSocketHandler.class);
+    private TapoSocketData socketData = new TapoSocketData();
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    public TapoSocketHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    @Override
+    public void newDataResult(String queryCommand) {
+        super.newDataResult(queryCommand);
+        switch (queryCommand) {
+            case DEVICE_CMD_GETINFO:
+                socketData = connector.getResponseData(TapoSocketData.class);
+                updateChannels(socketData);
+                break;
+            default:
+                responsePasstrough(connector.getResponseData(TapoResponse.class));
+                break;
+        }
+    }
+
+    /*****************************
+     * HANDLE COMMANDS
+     *****************************/
+
+    /**
+     * handle command sent to device
+     *
+     * @param channelUID channelUID command is sent to
+     * @param command command to be sent
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        /* perform actions */
+        if (command instanceof RefreshType) {
+            queryDeviceData();
+        } else if (command instanceof OnOffType) {
+            handleOnOffCommand(command);
+        } else {
+            logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+        }
+    }
+
+    private void handleOnOffCommand(Command command) {
+        switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+    }
+
+    /*****************************
+     * SEND COMMANDS
+     *****************************/
+
+    /**
+     * Switch device On or Off
+     * 
+     * @param on if true device will switch on. Otherwise switch off
+     */
+    protected void switchOnOff(boolean on) {
+        socketData.switchOnOff(on);
+        connector.sendCommandAndQuery(socketData, true);
+    }
+
+    /*****************************
+     * UPDATE CHANNELS
+     *****************************/
+
+    protected void updateChannels(TapoSocketData deviceData) {
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceData.isOn()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceData.getSignalLevel()));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getTimeType(deviceData.getOnTime(), Units.SECOND));
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceData.isOverheated()));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketStripHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketStripHandler.java
new file mode 100644 (file)
index 0000000..83a0860
--- /dev/null
@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.devices.wifi.socket;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildList;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSocketStripHandler extends TapoBaseDeviceHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoSocketStripHandler.class);
+    private TapoChildList tapoChildList = new TapoChildList();
+    private TapoSocketData socketData = new TapoSocketData();
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    public TapoSocketStripHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+     * received
+     * 
+     * @param queryCommand command where new data belong to
+     */
+    @Override
+    public void newDataResult(String queryCommand) {
+        super.newDataResult(queryCommand);
+        switch (queryCommand) {
+            case DEVICE_CMD_GETINFO:
+                baseDeviceData = connector.getResponseData(TapoBaseDeviceData.class);
+                updateChannels(baseDeviceData);
+                break;
+            case DEVICE_CMD_GETCHILDDEVICELIST:
+                tapoChildList = connector.getResponseData(TapoChildList.class);
+                updateChildDevices(tapoChildList);
+                break;
+            default:
+                logger.warn("({}) unhandled queryCommand '{}'", uid, queryCommand);
+        }
+    }
+
+    /**
+     * query device Properties
+     */
+    @Override
+    public void queryDeviceData() {
+        deviceError.reset();
+        if (isLoggedIn(LOGIN_RETRIES)) {
+            List<TapoRequest> requests = new ArrayList<>();
+            requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+            requests.add(new TapoRequest(DEVICE_CMD_GETCHILDDEVICELIST));
+            connector.sendMultipleRequest(requests);
+        }
+    }
+
+    /**
+     * Get ChildData by position (index)
+     */
+    private Optional<TapoChildDeviceData> getChildData(int position) {
+        return tapoChildList.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
+    }
+
+    /*****************************
+     * HANDLE COMMANDS
+     *****************************/
+
+    /**
+     * handle command sent to device
+     *
+     * @param channelUID channelUID command is sent to
+     * @param command command to be sent
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        /* perform actions */
+        if (command instanceof RefreshType) {
+            queryDeviceData();
+        } else if (command instanceof OnOffType) {
+            handleOnOffCommand(channelUID, command);
+        } else {
+            logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+        }
+    }
+
+    private void handleOnOffCommand(ChannelUID channelUID, Command command) {
+        String id = channelUID.getIdWithoutGroup();
+        Boolean targetState = command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE;
+        if (id.startsWith(CHANNEL_OUTPUT)) { // Command is sent to a child's device output
+            Integer index = Integer.valueOf(id.replace(CHANNEL_OUTPUT, ""));
+            switchOnOff(targetState, index);
+        }
+    }
+
+    /*****************************
+     * SEND COMMANDS
+     *****************************/
+
+    /**
+     * Switch child On or Off
+     * 
+     * @param on if true device will switch on. Otherwise switch off
+     * @param index index of child device
+     */
+    protected void switchOnOff(boolean on, int index) {
+        socketData.switchOnOff(on);
+
+        getChildData(index).ifPresent(child -> {
+            child.setDeviceOn(on);
+            connector.sendChildCommand(child, true);
+        });
+    }
+
+    /*****************************
+     * UPDATE CHANNELS
+     *****************************/
+
+    /**
+     * Set all device Childs data to device
+     */
+    public void updateChildDevices(TapoChildList hostData) {
+        hostData.getChildDeviceList().forEach(child -> {
+            updateChildDevice(child.getPosition(), child);
+        });
+    }
+
+    /**
+     * Set data to child with index (position) x
+     */
+    public void updateChildDevice(int index, TapoChildDeviceData child) {
+        updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT + index), getOnOffType(child.isOn()));
+    }
+
+    /**
+     * Update Channels
+     */
+    protected void updateChannels(TapoBaseDeviceData deviceData) {
+        updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceData.getSignalLevel()));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoChildDiscoveryService.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoChildDiscoveryService.java
new file mode 100644 (file)
index 0000000..b641fb6
--- /dev/null
@@ -0,0 +1,205 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.discovery;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.hub.TapoHubHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO Smart Home thing discovery
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoChildDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+    private final Logger logger = LoggerFactory.getLogger(TapoChildDiscoveryService.class);
+    protected @NonNullByDefault({}) TapoHubHandler hub;
+    private boolean backgroundDiscoveryEnabled = false;
+    private String uid = "";
+
+    /***********************************
+     *
+     * INITIALIZATION
+     *
+     ************************************/
+
+    /**
+     * INIT CLASS
+     * 
+     */
+    public TapoChildDiscoveryService() {
+        super(SUPPORTED_HUB_CHILD_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
+    }
+
+    /**
+     * deactivate
+     */
+    @Override
+    public void deactivate() {
+        super.deactivate();
+        logger.trace("({}) DiscoveryService deactivated", uid);
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof TapoHubHandler hubHandler) {
+            TapoHubHandler tapoHub = hubHandler;
+            tapoHub.setDiscoveryService(this);
+            hub = tapoHub;
+            uid = hub.getUID().toString();
+            activate();
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return this.hub;
+    }
+
+    /**
+     * Enable or disable backgrounddiscovery
+     */
+    public void setBackGroundDiscovery(boolean enableService) {
+        backgroundDiscoveryEnabled = enableService;
+        if (enableService) {
+            super.startBackgroundDiscovery();
+        } else {
+            super.stopBackgroundDiscovery();
+        }
+    }
+
+    @Override
+    public boolean isBackgroundDiscoveryEnabled() {
+        return backgroundDiscoveryEnabled;
+    }
+
+    /***********************************
+     *
+     * SCAN HANDLING
+     *
+     ************************************/
+
+    /**
+     * Start scan manually
+     */
+    @Override
+    public void startScan() {
+        removeOlderResults(getTimestampOfLastScan());
+        if (hub != null) {
+            logger.trace("({}) DiscoveryService scan started", uid);
+            hub.queryDeviceData();
+        }
+    }
+
+    @Override
+    public synchronized void stopScan() {
+        super.stopScan();
+        thingsDiscovered(hub.getChildDevices());
+        logger.trace("({}) DiscoveryService scan stoped", uid);
+    }
+
+    /***********************************
+     *
+     * handle Results
+     *
+     ************************************/
+
+    /*
+     * create discoveryResults and discovered things
+     */
+    public void thingsDiscovered(List<TapoChildDeviceData> resultData) {
+        resultData.forEach(child -> {
+            ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, child.getModel());
+            if (SUPPORTED_HUB_CHILD_TYPES_UIDS.contains(thingTypeUID)) {
+                DiscoveryResult discoveryResult = createResult(child);
+                thingDiscovered(discoveryResult);
+            } else {
+                logger.debug("({}) Discovered unsupportet ThingType '{}'", uid, thingTypeUID);
+            }
+        });
+    }
+
+    /**
+     * create discoveryResult (Thing) from TapoChild Object
+     */
+    public DiscoveryResult createResult(TapoChildDeviceData child) {
+        TapoHubHandler tapoHub = this.hub;
+        String deviceModel = child.getModel();
+        String deviceSerial = child.getDeviceId();
+        String label = getDeviceLabel(child);
+        ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+        /* create properties */
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
+        properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(child.getMAC(), MAC_DIVISION_CHAR));
+        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, child.getFirmwareVersion());
+        properties.put(Thing.PROPERTY_HARDWARE_VERSION, child.getHardwareVersion());
+        properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
+        properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceSerial);
+
+        logger.debug("({}) device of type '{}' discovered with serial'{}'", uid, deviceModel, deviceSerial);
+        if (tapoHub != null) {
+            ThingUID bridgeUID = tapoHub.getUID();
+            ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceSerial);
+            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                    .withRepresentationProperty(CHILD_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
+                    .build();
+        } else {
+            ThingUID thingUID = new ThingUID(BINDING_ID, deviceSerial);
+            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                    .withRepresentationProperty(CHILD_REPRESENTATION_PROPERTY).withLabel(label).build();
+        }
+    }
+
+    /**
+     * Get devicelabel from from TapoChild Object
+     */
+    protected String getDeviceLabel(TapoChildDeviceData child) {
+        try {
+            String deviceLabel = "";
+            String deviceModel = child.getModel();
+            ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+            if (SUPPORTED_SMART_CONTACTS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_SMART_CONTACT;
+            } else if (SUPPORTED_MOTION_SENSORS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_MOTION_SENSOR;
+            }
+            return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
+        } catch (Exception e) {
+            logger.debug("({}) error getDeviceLabel", uid, e);
+            return "";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoDiscoveryService.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoDiscoveryService.java
new file mode 100644 (file)
index 0000000..f262836
--- /dev/null
@@ -0,0 +1,285 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.discovery;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeConfiguration;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoDeviceConfiguration;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResult;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResultList;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO Smart Home thing discovery
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+    private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
+    private final String uid;
+    protected @NonNullByDefault({}) TapoBridgeHandler bridge;
+    protected @NonNullByDefault({}) TapoUdpDiscovery udpDiscovery;
+    protected @NonNullByDefault({}) TapoCloudConnector cloudConnector;
+    private @NonNullByDefault({}) TapoBridgeConfiguration config;
+    private @Nullable ScheduledFuture<?> discoveryJob;
+    private TapoDiscoveryResultList discoveryResultList = new TapoDiscoveryResultList();
+
+    /***********************************
+     *
+     * INITIALIZATION
+     *
+     ************************************/
+
+    public TapoDiscoveryService() {
+        super(SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
+        uid = "Discovery-Service";
+    }
+
+    @Override
+    public void activate() {
+        config = bridge.getBridgeConfig();
+        if (config.cloudDiscovery || config.udpDiscovery) {
+            startBackgroundDiscovery();
+        }
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+        stopScheduler(discoveryJob);
+    }
+
+    @Override
+    public void startBackgroundDiscovery() {
+        startDiscoveryScheduler();
+    }
+
+    @Override
+    public void stopBackgroundDiscovery() {
+        stopScheduler(discoveryJob);
+    }
+
+    /**
+     * setThingHandler - set bridge and other handlers on initializing service
+     */
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof TapoBridgeHandler bridgeHandler) {
+            TapoBridgeHandler tapoBridge = bridgeHandler;
+            tapoBridge.setDiscoveryService(this);
+            bridge = tapoBridge;
+            udpDiscovery = new TapoUdpDiscovery(bridge);
+            cloudConnector = bridgeHandler.getCloudConnector();
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return this.bridge;
+    }
+
+    /*************************
+     * SCHEDULER
+     *************************/
+    /**
+     * Start DeviceDiscovery Scheduler
+     */
+    protected void startDiscoveryScheduler() {
+        config = bridge.getBridgeConfig();
+        int pollingInterval = config.discoveryInterval;
+        TimeUnit timeUnit = TimeUnit.MINUTES;
+        if ((config.cloudDiscovery || config.udpDiscovery) && pollingInterval > 0) {
+            logger.debug("{} starting discoveryScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
+
+            this.discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, pollingInterval, timeUnit);
+        } else {
+            logger.debug("({}) discoveryScheduler disabled with config '0'", uid);
+            stopScheduler(this.discoveryJob);
+        }
+    }
+
+    /**
+     * Stop scheduler
+     * 
+     * @param scheduler ScheduledFeature which should be stopped
+     */
+    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+        if (scheduler != null) {
+            scheduler.cancel(true);
+            scheduler = null;
+        }
+    }
+
+    /***********************************
+     *
+     * SCAN HANDLING
+     *
+     ************************************/
+
+    /**
+     * Start scan manually
+     */
+    @Override
+    public void startScan() {
+        logger.trace("{} starting scan", this.uid);
+        removeOlderResults(getTimestampOfLastScan());
+        discoveryResultList.clear();
+        try {
+            if (bridge != null) {
+                bridge.getErrorHandler().reset();
+                if (config.cloudDiscovery && bridge.loginCloud()) {
+                    cloudConnector.getDeviceList();
+                }
+                if (config.udpDiscovery && udpDiscovery != null) {
+                    udpDiscovery.startScan();
+                }
+                handleDiscoveryList(discoveryResultList);
+            }
+        } catch (Exception e) {
+            logger.debug("({}) scan failed: ", uid, e);
+        }
+    }
+
+    @Override
+    public void abortScan() {
+        logger.trace("{} scan aborted", this.uid);
+    }
+
+    @Override
+    public void stopScan() {
+        logger.trace("{} scan stoped", this.uid);
+    }
+
+    /***********************************
+     *
+     * handle Results
+     *
+     ************************************/
+
+    /*
+     * add scan results to discoveryResultList
+     */
+    public void addScanResults(TapoDiscoveryResultList deviceList) {
+        if (discoveryResultList.size() == 0) {
+            discoveryResultList = deviceList;
+        } else {
+            for (TapoDiscoveryResult result : deviceList) {
+                addScanResult(result);
+            }
+        }
+    }
+
+    /**
+     * add singleResult to list
+     */
+    public void addScanResult(TapoDiscoveryResult device) {
+        discoveryResultList.addResult(device);
+    }
+
+    /**
+     * work with result from get devices from deviceList
+     * 
+     * @param deviceList
+     */
+    protected void handleDiscoveryList(TapoDiscoveryResultList deviceList) {
+        logger.trace("{} handle discovery result", this.uid);
+        try {
+            for (TapoDiscoveryResult deviceElement : deviceList) {
+                if (!deviceElement.deviceMac().isBlank()) {
+                    String deviceModel = getDeviceModel(deviceElement);
+                    String deviceLabel = getDeviceLabel(deviceElement);
+                    ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+                    /* create thing */
+                    if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
+                        if (!config.onlyLocalOnlineDevices || deviceElement.ip().length() > 1) {
+                            DiscoveryResult discoveryResult = createResult(deviceElement);
+                            thingDiscovered(discoveryResult);
+                        } else {
+                            logger.debug("{} device discovered but is offline '{}'", this.uid, deviceLabel);
+                        }
+                    } else {
+                        logger.debug("{} unsupported device discovered '{}'", this.uid, deviceLabel);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.debug("({}) error handle DiscoveryResult", uid, e);
+        }
+    }
+
+    /**
+     * CREATE DISCOVERY RESULT
+     * creates discoveryResult (Thing) from JsonObject got from Cloud
+     * 
+     * @param device JsonObject with device information
+     * @return DiscoveryResult-Object
+     */
+    private DiscoveryResult createResult(TapoDiscoveryResult device) {
+        TapoBridgeHandler tapoBridge = this.bridge;
+        String deviceModel = getDeviceModel(device);
+        String label = getDeviceLabel(device);
+        String deviceMAC = device.deviceMac();
+        ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+        /* create properties */
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
+        properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(deviceMAC, MAC_DIVISION_CHAR));
+        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.fwVer());
+        properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.deviceHwVer());
+        properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
+        properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.deviceId());
+        if (device.ip().length() >= 7) {
+            properties.put(TapoDeviceConfiguration.CONFIG_DEVICE_IP, device.ip());
+            properties.put(TapoDeviceConfiguration.CONFIG_HTTP_PORT, device.encryptionShema().httpPort());
+            properties.put(TapoDeviceConfiguration.CONFIG_PROTOCOL, device.encryptionShema().encryptType());
+        }
+
+        logger.debug("device {} discovered with mac {}", deviceModel, deviceMAC);
+        if (tapoBridge != null) {
+            ThingUID bridgeUID = tapoBridge.getUID();
+            ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
+            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                    .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
+                    .build();
+        } else {
+            ThingUID thingUID = new ThingUID(BINDING_ID, deviceMAC);
+            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                    .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withLabel(label).build();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUdpDiscovery.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUdpDiscovery.java
new file mode 100644 (file)
index 0000000..31adcf4
--- /dev/null
@@ -0,0 +1,267 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.discovery;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResult;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoKeyPair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO Smart Home device UDP-connections.
+ * THIS IS FOR TESTING
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoUdpDiscovery {
+    private final Logger logger = LoggerFactory.getLogger(TapoUdpDiscovery.class);
+    private final TapoBridgeHandler bridge;
+    private String uid;
+
+    private static final Integer BROADCAST_TIMEOUT_MS = 8000;
+    private static final int[] BROADCAST_DISCOVERY_PORTS = { 9999, 20002 };
+    private static final int TDP_CHECKSUM_DEFAULT = 1516993677;
+    private static final int TDP_RANDOM_BOUND = 268435456;
+    private static final int BUFFER_SIZE = 2048;
+    private static final int RSA_KEYSIZE = 2048;
+    private static final String RSA_MESSAGE_KEY = "rsa_key";
+    private static final String RSA_MESSAGE_START_BYTES = "0200000100001100";
+
+    public TapoUdpDiscovery(TapoBridgeHandler bridge) {
+        this.bridge = bridge;
+        uid = bridge.getUID().getAsString() + " - updDiscovery";
+    }
+
+    /***********************************
+     * SCAN HANDLING
+     ************************************/
+
+    /**
+     * start scan with configured broadcast address
+     */
+    protected void startScan() throws TapoErrorHandler {
+        String broadcastAddress = bridge.getBridgeConfig().broadcastAddress;
+        try {
+            scanAllPorts(InetAddress.getByName(broadcastAddress));
+        } catch (UnknownHostException e) {
+            logger.debug("({}) unknown host exception {}", uid, broadcastAddress);
+            throw new TapoErrorHandler(TapoErrorCode.ERR_CONFIG_IP, "unknown broadcast address");
+        }
+    }
+
+    /**
+     * Scan all available interfaces and ports
+     */
+    protected void scanAllInterfaces() throws TapoErrorHandler {
+        List<InetAddress> broadcastList = listAllBroadcastAddresses();
+        for (InetAddress inetAddress : broadcastList) {
+            scanAllPorts(inetAddress);
+        }
+    }
+
+    /**
+     * Scan all defined ports of an specific broadcastAddress
+     * 
+     * @param broadcastAddress
+     */
+    protected void scanAllPorts(InetAddress broadcastAddress) throws TapoErrorHandler {
+        for (int port : BROADCAST_DISCOVERY_PORTS) {
+            udpScan(getBroadcastMessage(port), broadcastAddress, port);
+        }
+    }
+
+    /**
+     * 
+     * @param sendData
+     * @param broadcastAddress
+     * @param discoveryPort
+     * @throws TapoErrorHandler
+     */
+    private void udpScan(byte[] message, InetAddress broadcastAddress, int discoveryPort) throws TapoErrorHandler {
+        logger.trace("({}) startUdpScan with address: '{}' to port {}", uid, broadcastAddress, discoveryPort);
+        try {
+            DatagramSocket udpSocket = new DatagramSocket();
+            udpSocket.setSoTimeout(BROADCAST_TIMEOUT_MS);
+            udpSocket.setBroadcast(true);
+
+            logger.trace("({}) send broadcast ('{}' byte) packet '{}'", uid, message.length, byteArrayToHex(message));
+
+            DatagramPacket sendPacket = new DatagramPacket(message, message.length, broadcastAddress, discoveryPort);
+
+            udpSocket.send(sendPacket);
+
+            while (!udpSocket.isClosed()) {
+                // Wait for a response
+                byte[] recvBuf = new byte[BUFFER_SIZE];
+                DatagramPacket receivePacket;
+                try {
+                    receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
+                    udpSocket.receive(receivePacket);
+                    handleDiscoveryResult(receivePacket);
+                } catch (SocketTimeoutException e) {
+                    logger.trace("({}) socketTimeOutException", uid);
+                    udpSocket.close();
+                }
+            }
+            udpSocket.close();
+        } catch (Exception e) {
+            logger.debug("({}) scan failed: '{}'", uid, e.getMessage());
+            throw new TapoErrorHandler(e);
+        }
+    }
+
+    /***********************************
+     * PRIVATE HELPERS
+     ************************************/
+
+    /*
+     * List all Broadcast-Addresses from all Interfaces
+     */
+    private List<InetAddress> listAllBroadcastAddresses() throws TapoErrorHandler {
+        try {
+            List<InetAddress> broadcastList = new ArrayList<>();
+            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+            while (interfaces.hasMoreElements()) {
+                @Nullable
+                NetworkInterface networkInterface = interfaces.nextElement();
+                if (networkInterface.isLoopback() || !networkInterface.isUp()) {
+                    continue;
+                }
+                networkInterface.getInterfaceAddresses().stream().map(a -> a.getBroadcast()).filter(Objects::nonNull)
+                        .forEach(broadcastList::add);
+            }
+            return broadcastList;
+        } catch (Exception e) {
+            logger.debug("({}) socket exception", uid);
+            throw new TapoErrorHandler(e);
+        }
+    }
+
+    /**
+     * 
+     * @param port
+     * @return
+     */
+    private byte[] getBroadcastMessage(int port) throws TapoErrorHandler {
+        String message;
+        byte[] messageBytes = {};
+        try {
+            switch (port) {
+                case 9999:
+                    // message = "'system': {'get_sysinfo': None}"; // unencrypted message
+                    message = "d0f281f88bff9af7d5ef94b6d1b4c09fec95e68fe187e8caf08bf68ba785e688cba7c8bdd9fbc1ba98ff9aeeb1d8b6d0bf9da7dca1dcf0d2a1ccaddfabc7aec8ad83ea85f1dfbcd3bed3bcd2fc9ff39ce98daf95eeccabcebae58ce284ebc9f388f588a486f598f98bff93fa9cf9d7b4d5b896ff8fec8de085f796b8dbb7d8adc9ebd1aa88ef8afea1c8a6c0af8db7ccb1ccb1";
+                    messageBytes = hexStringToByteArray(message);
+                    break;
+                case 20002:
+                    messageBytes = buildRsaPacket();
+                    break;
+                case 20004:
+                    messageBytes = buildRsaPacket();
+                    break;
+            }
+            return messageBytes;
+        } catch (Exception e) {
+            throw new TapoErrorHandler(e);
+        }
+    }
+
+    /*
+     * build discoverypacket with rsa key-message
+     */
+    private byte[] buildRsaPacket() throws TapoErrorHandler {
+        String message;
+
+        /* build jsonObject with rsa key */
+        JsonObject parameters = new JsonObject();
+        JsonObject messageObject = new JsonObject();
+
+        parameters.addProperty(RSA_MESSAGE_KEY, new TapoKeyPair(RSA_KEYSIZE).getPublicKey());
+        messageObject.add("params", parameters);
+        message = messageObject.toString();
+        logger.trace("({}) discovery-message: '{}'", uid, message);
+
+        return buildByteMessage(message);
+    }
+
+    /*
+     * build message prefix dynamic insert bytes that change based on the string, randomness and crc
+     * algorithm found by reverse-engineering android app in com\tplink\tdp\common\b.java
+     */
+    private byte[] buildByteMessage(String message) throws TapoErrorHandler {
+        byte[] replace;
+        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
+        byte[] fullPacket = new byte[messageBytes.length + 16];
+
+        /* put startvalues and message */
+        System.arraycopy(hexStringToByteArray(RSA_MESSAGE_START_BYTES), 0, fullPacket, 0, 8);
+        System.arraycopy(messageBytes, 0, fullPacket, 16, messageBytes.length);
+
+        /* replace with message lenght */
+        replace = shortToByteArray((short) messageBytes.length, ByteOrder.BIG_ENDIAN);
+        System.arraycopy(replace, 0, fullPacket, 4, 2);
+
+        /* replace with a random array */
+        replace = intToByteArray(new Random().nextInt(TDP_RANDOM_BOUND) + 0, ByteOrder.BIG_ENDIAN);
+        System.arraycopy(replace, 0, fullPacket, 8, 4);
+
+        /* replace with checksum default */
+        replace = intToByteArray(TDP_CHECKSUM_DEFAULT, ByteOrder.BIG_ENDIAN);
+        System.arraycopy(replace, 0, fullPacket, 12, 4);
+
+        /* replace crc */
+        replace = intToByteArray((int) crc32Checksum(fullPacket), ByteOrder.BIG_ENDIAN);
+        System.arraycopy(replace, 0, fullPacket, 12, 4);
+
+        return fullPacket;
+    }
+
+    /**
+     * Handle discoveryresult and add to resultList
+     * 
+     * @param receivePacket
+     */
+    private void handleDiscoveryResult(DatagramPacket receivePacket) {
+        String responseMessage = new String(receivePacket.getData(), StandardCharsets.UTF_8);
+        logger.trace("({}) received responseMessage: '{}'", uid, responseMessage);
+        responseMessage = responseMessage.substring(responseMessage.indexOf("{")).trim();
+        TapoResponse tapoResponse = getObjectFromJson(responseMessage, TapoResponse.class);
+        bridge.getDiscoveryService().addScanResult(getObjectFromJson(tapoResponse.result(), TapoDiscoveryResult.class));
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResult.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResult.java
new file mode 100644 (file)
index 0000000..c48944f
--- /dev/null
@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.discovery.dto;
+
+import static org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolEnum.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.isBase64Encoded;
+
+import java.util.Base64;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * TapoDiscoveryResult Data Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoDiscoveryResult(@Expose @SerializedName("factory_default") boolean factoryDefault,
+        @Expose @SerializedName("is_support_iot_cloud") boolean isSupportIOT,
+        @Expose @SerializedName("mgt_encrypt_schm") EncryptionShema encryptionShema, @Expose int role,
+        @Expose int status, @Expose String alias, @Expose String appServerUrl, @Expose String deviceHwVer,
+        @Expose @SerializedName(value = "deviceID", alternate = "device_id") String deviceId,
+        @Expose @SerializedName(value = "deviceMac", alternate = "mac") String deviceMac,
+        @Expose @SerializedName(value = "deviceModel", alternate = "device_model") String deviceModel,
+        @Expose String deviceName, @Expose String deviceRegion,
+        @Expose @SerializedName(value = "deviceType", alternate = "device_type") String deviceType, @Expose String fwId,
+        @Expose String fwVer, @Expose String hwId, @Expose String ip, @Expose String isSameRegion,
+        @Expose String oemId) {
+
+    public record EncryptionShema(@Expose @SerializedName("is_support_https") boolean isSupportHttps,
+            @Expose @SerializedName("encrypt_type") String encryptType,
+            @Expose @SerializedName("http_port") int httpPort, @Expose int lv2) {
+    }
+
+    /* init new emty record */
+
+    public TapoDiscoveryResult() {
+        this(false, false, new EncryptionShema(false, SECUREPASSTROUGH.toString(), 80, 0), 0, 0, "", "", "", "", "", "",
+                "", "", "", "", "", "", "", "", "");
+    }
+
+    /**********************************************
+     * Return default data if recordobject is null
+     **********************************************/
+
+    @Override
+    public boolean factoryDefault() {
+        return Objects.requireNonNullElse(factoryDefault, false);
+    }
+
+    @Override
+    public boolean isSupportIOT() {
+        return Objects.requireNonNullElse(isSupportIOT, false);
+    }
+
+    @Override
+    public int role() {
+        return Objects.requireNonNullElse(role, 0);
+    }
+
+    @Override
+    public int status() {
+        return Objects.requireNonNullElse(status, 0);
+    }
+
+    @Override
+    public String alias() {
+        String encodedAlias = Objects.requireNonNullElse(alias, "");
+
+        if (isBase64Encoded(encodedAlias)) {
+            return new String(Base64.getDecoder().decode(encodedAlias));
+        } else {
+            return alias;
+        }
+    }
+
+    @Override
+    public String appServerUrl() {
+        return Objects.requireNonNullElse(appServerUrl, "");
+    }
+
+    @Override
+    public String deviceHwVer() {
+        return Objects.requireNonNullElse(deviceHwVer, "");
+    }
+
+    @Override
+    public String deviceId() {
+        return Objects.requireNonNullElse(deviceId, "");
+    }
+
+    @Override
+    public String deviceMac() {
+        String mac = Objects.requireNonNullElse(deviceMac, "");
+        return TapoUtils.unformatMac(mac).toUpperCase();
+    }
+
+    @Override
+    public String deviceModel() {
+        return Objects.requireNonNullElse(deviceModel, "");
+    }
+
+    @Override
+    public String deviceName() {
+        return Objects.requireNonNullElse(deviceName, "");
+    }
+
+    @Override
+    public String deviceRegion() {
+        return Objects.requireNonNullElse(deviceRegion, "");
+    }
+
+    @Override
+    public String deviceType() {
+        return Objects.requireNonNullElse(deviceType, "");
+    }
+
+    @Override
+    public EncryptionShema encryptionShema() {
+        return Objects.requireNonNullElse(encryptionShema,
+                new EncryptionShema(false, SECUREPASSTROUGH.toString(), 80, 0));
+    }
+
+    @Override
+    public String fwId() {
+        return Objects.requireNonNullElse(fwId, "");
+    }
+
+    @Override
+    public String fwVer() {
+        return Objects.requireNonNullElse(fwVer, "");
+    }
+
+    @Override
+    public String hwId() {
+        return Objects.requireNonNullElse(hwId, "");
+    }
+
+    @Override
+    public String ip() {
+        return Objects.requireNonNullElse(ip, "");
+    }
+
+    @Override
+    public String isSameRegion() {
+        return Objects.requireNonNullElse(isSameRegion, "");
+    }
+
+    @Override
+    public String oemId() {
+        return Objects.requireNonNullElse(oemId, "");
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResultList.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResultList.java
new file mode 100644 (file)
index 0000000..21b42c8
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.discovery.dto;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * TapoCloud DeviceList Data Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDiscoveryResultList implements Iterable<TapoDiscoveryResult> {
+
+    @Expose
+    private List<TapoDiscoveryResult> deviceList = new ArrayList<>(0);
+
+    /* init new emty list */
+    public TapoDiscoveryResultList() {
+    }
+
+    /**
+     * Add device to devicelist. Overwrite fields of device with new data if already exists and field has values
+     * 
+     * @param result
+     */
+    public void addResult(TapoDiscoveryResult result) {
+        TapoDiscoveryResult old = getDeviceByMac(result.deviceMac());
+        if (old == null) {
+            deviceList.add(result);
+        } else {
+            /* create new discoveryresult and overwrite empty values */
+            boolean factoryDefault = compareValuesAgainstComparator(old.factoryDefault(), result.factoryDefault(),
+                    false);
+            boolean isSupportIOT = compareValuesAgainstComparator(old.isSupportIOT(), result.isSupportIOT(), false);
+            TapoDiscoveryResult.EncryptionShema enctyptionShema = compareValuesAgainstComparator(old.encryptionShema(),
+                    result.encryptionShema(), new TapoDiscoveryResult.EncryptionShema(false, "", 0, 0));
+            int role = compareValuesAgainstComparator(old.role(), result.role(), 0);
+            int status = compareValuesAgainstComparator(old.status(), result.status(), 0);
+            String alias = compareValuesAgainstComparator(old.alias(), result.alias(), "");
+            String appServerUrl = compareValuesAgainstComparator(old.alias(), result.alias(), "");
+            String deviceHwVer = compareValuesAgainstComparator(old.deviceHwVer(), result.deviceHwVer(), "");
+            String deviceId = compareValuesAgainstComparator(old.deviceId(), result.deviceId(), "");
+            String deviceMac = compareValuesAgainstComparator(old.deviceMac(), result.deviceMac(), "");
+            String deviceModel = compareValuesAgainstComparator(old.deviceModel(), result.deviceModel(), "");
+            String deviceRegion = compareValuesAgainstComparator(old.deviceRegion(), result.deviceRegion(), "");
+            String deviceType = compareValuesAgainstComparator(old.deviceType(), result.deviceType(), "");
+            String fwId = compareValuesAgainstComparator(old.fwId(), result.fwId(), "");
+            String fwVer = compareValuesAgainstComparator(old.fwVer(), result.fwVer(), "");
+            String hwId = compareValuesAgainstComparator(old.hwId(), result.hwId(), "");
+            String ip = compareValuesAgainstComparator(old.ip(), result.ip(), "");
+            String isSameRegion = compareValuesAgainstComparator(old.isSameRegion(), result.isSameRegion(), "");
+            String oemId = compareValuesAgainstComparator(old.oemId(), result.oemId(), "");
+
+            /* add new result */
+            deviceList.remove(old);
+            deviceList.add(new TapoDiscoveryResult(factoryDefault, isSupportIOT, enctyptionShema, role, status, alias,
+                    appServerUrl, deviceHwVer, deviceId, deviceMac, deviceModel, deviceModel, deviceRegion, deviceType,
+                    fwId, fwVer, hwId, ip, isSameRegion, oemId));
+        }
+    }
+
+    public void clear() {
+        deviceList = new ArrayList<>(0);
+    }
+
+    /*
+     * check if list contains element with mac
+     */
+    public boolean containsDeviceWithMac(final String mac) {
+        return deviceList.stream().anyMatch(o -> mac.equals(o.deviceMac()));
+    }
+
+    /*
+     * return element which contains mac
+     */
+    public @Nullable TapoDiscoveryResult getDeviceByMac(final String mac) {
+        return deviceList.stream().filter(o -> mac.equals(o.deviceMac())).findFirst().orElse(null);
+    }
+
+    public List<TapoDiscoveryResult> deviceList() {
+        return Objects.requireNonNullElse(deviceList, List.of());
+    }
+
+    @Override
+    public Iterator<TapoDiscoveryResult> iterator() {
+        return deviceList.iterator();
+    }
+
+    public int size() {
+        return deviceList.size();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoBaseRequestInterface.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoBaseRequestInterface.java
new file mode 100644 (file)
index 0000000..bbad8ca
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.dto;
+
+/**
+ * Interface for main requests sent to devices
+ *
+ * @author Christian Wild - Initial contribution
+ */
+public interface TapoBaseRequestInterface {
+    public String method();
+
+    public Object params();
+
+    public long requestTimeMils();
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoChildRequest.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoChildRequest.java
new file mode 100644 (file)
index 0000000..8704ce7
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+import io.reactivex.annotations.Nullable;
+
+/**
+ * Holds child data sent to device
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Christian Wild - Code revision
+ */
+@NonNullByDefault
+public record TapoChildRequest(@Expose String method, @Expose @Nullable Object params,
+        @Expose long requestTimeMils) implements TapoBaseRequestInterface {
+
+    private record ControlRequest(@Expose @SerializedName("device_id") String deviceId, @Expose Object requestData) {
+    }
+
+    /**
+     * Create request to control child devices
+     */
+    public TapoChildRequest(TapoChildDeviceData deviceData) {
+        this(DEVICE_CMD_CONTROL_CHILD,
+                new ControlRequest(deviceData.getDeviceId(), new TapoRequest(DEVICE_CMD_SETINFO, deviceData)),
+                System.currentTimeMillis());
+    }
+
+    /***********************************************
+     * RETURN VALUES
+     **********************************************/
+
+    @Override
+    public String toString() {
+        return toJson();
+    }
+
+    public String toJson() {
+        return GSON.toJson(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoMultipleRequest.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoMultipleRequest.java
new file mode 100644 (file)
index 0000000..1c08d3f
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+import io.reactivex.annotations.Nullable;
+
+/**
+ * Holds multi-request-data sent to device
+ *
+ * @author Christian Wild - Changed SubRequest top MultiRequest
+ */
+@NonNullByDefault
+public record TapoMultipleRequest(@Expose String method, @Expose @Nullable Object params,
+        @Expose long requestTimeMils) implements TapoBaseRequestInterface {
+
+    public record SubRequest(@Expose List<TapoRequest> requests) {
+    }
+
+    public TapoMultipleRequest(TapoRequest... requests) {
+        this(DEVICE_CMD_MULTIPLE_REQ, new SubRequest(Arrays.asList(requests)), System.currentTimeMillis());
+    }
+
+    public TapoMultipleRequest(List<TapoRequest> requests) {
+        this(DEVICE_CMD_MULTIPLE_REQ, new SubRequest(requests), System.currentTimeMillis());
+    }
+
+    /***********************************************
+     * RETURN VALUES
+     **********************************************/
+
+    @Override
+    public String toString() {
+        return toJson();
+    }
+
+    public String toJson() {
+        return GSON.toJson(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoRequest.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoRequest.java
new file mode 100644 (file)
index 0000000..5751067
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.Expose;
+
+import io.reactivex.annotations.Nullable;
+
+/**
+ * Holds data sent to device
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoRequest(@Expose String method, @Expose @Nullable Object params,
+        @Expose long requestTimeMils) implements TapoBaseRequestInterface {
+
+    /**
+     * Create request with command (method) and data (params) sent to device
+     */
+    public TapoRequest(String method, Object params) {
+        this(method, params, System.currentTimeMillis());
+    }
+
+    /**
+     * Create request with command (method) sent to device
+     */
+    public TapoRequest(String method) {
+        this(method, "", System.currentTimeMillis());
+    }
+
+    /***********************************************
+     * RETURN VALUES
+     **********************************************/
+
+    @Override
+    public String toString() {
+        return toJson();
+    }
+
+    public String toJson() {
+        return new GsonBuilder().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation().create().toJson(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoResponse.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoResponse.java
new file mode 100644 (file)
index 0000000..cfdebc7
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Tapo-Response Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoResponse(@Expose @SerializedName(value = "errorcode", alternate = "error_code") int errorCode,
+        @Expose @SerializedName("result") JsonObject result, @Expose String method,
+        @Expose @SerializedName("msg") String message) {
+
+    private static final String MULTI_RESPONSE_KEY = "responses";
+
+    public TapoResponse() {
+        this(0, new JsonObject(), "", "");
+    }
+
+    public TapoResponse(int errorCode) {
+        this(errorCode, new JsonObject(), "", "");
+    }
+
+    /***********************************************
+     * RETURN VALUES
+     * Return default data if recordobject is null
+     **********************************************/
+
+    public boolean hasError() {
+        return errorCode != 0;
+    }
+
+    public boolean isMultiRequestResponse() {
+        return result.has(MULTI_RESPONSE_KEY);
+    }
+
+    @Override
+    public int errorCode() {
+        return Objects.requireNonNullElse(errorCode, ERR_API_CLOUD_FAILED.getCode());
+    }
+
+    @Override
+    public JsonObject result() {
+        return Objects.requireNonNullElse(result, new JsonObject());
+    }
+
+    @Override
+    public String message() {
+        return Objects.requireNonNullElse(message, "");
+    }
+
+    @Override
+    public String method() {
+        return Objects.requireNonNullElse(method, "");
+    }
+
+    public List<TapoResponse> responses() {
+        JsonArray responses = result.getAsJsonArray(MULTI_RESPONSE_KEY);
+        Type repsonseListType = new TypeToken<List<TapoResponse>>() {
+        }.getType();
+        return Objects.requireNonNullElse(GSON.fromJson(responses.toString(), repsonseListType),
+                new ArrayList<TapoResponse>());
+    }
+
+    @Override
+    public String toString() {
+        return toJson();
+    }
+
+    public String toJson() {
+        return new GsonBuilder().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation().create().toJson(this);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java
deleted file mode 100644 (file)
index 056839b..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers;
-
-import static java.util.Base64.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * MimeEncoder
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class MimeEncode {
-
-    public byte[] encode(byte[] src) {
-        return getMimeEncoder().encode(src);
-    }
-
-    public String encodeToString(byte[] src) {
-        return getMimeEncoder().encodeToString(src);
-    }
-
-    public byte[] decode(byte[] src) {
-        return getMimeDecoder().decode(src);
-    }
-
-    public byte[] decode(String src) {
-        return getMimeDecoder().decode(src);
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/PayloadBuilder.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/PayloadBuilder.java
deleted file mode 100644 (file)
index fcf0599..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-
-/**
- * PAYLOAD BUILDER
- * Generates payload for TapoHttp request
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class PayloadBuilder {
-    public String method = "";
-    private JsonObject parameters = new JsonObject();
-
-    /**
-     * Set Command
-     *
-     * @param command command (method) to send
-     */
-    public void setCommand(String command) {
-        this.method = command;
-    }
-
-    /**
-     * Add Parameter
-     *
-     * @param name parameter name
-     * @param value parameter value (typeOf Bool,Number or String)
-     */
-    public void addParameter(String name, Object value) {
-        if (value instanceof Boolean bool) {
-            this.parameters.addProperty(name, bool);
-        } else if (value instanceof Number number) {
-            this.parameters.addProperty(name, number);
-        } else {
-            this.parameters.addProperty(name, value.toString());
-        }
-    }
-
-    /**
-     * Get JSON Payload (STRING)
-     *
-     * @return String JSON-Payload
-     */
-    public String getPayload() {
-        Gson gson = new Gson();
-        JsonObject payload = getJsonPayload();
-        return gson.toJson(payload);
-    }
-
-    /**
-     * Get JSON Payload (JSON-Object)
-     *
-     * @return JsonObject JSON-Payload
-     */
-    public JsonObject getJsonPayload() {
-        JsonObject payload = new JsonObject();
-        long timeMils = System.currentTimeMillis();// * 1000;
-
-        payload.addProperty("method", this.method);
-        if (this.parameters.size() > 0) {
-            payload.add("params", this.parameters);
-        }
-        payload.addProperty("requestTimeMils", timeMils);
-
-        return payload;
-    }
-
-    /**
-     * Flush Parameters
-     * remove all parameters
-     */
-    public void flushParameters(String command) {
-        parameters = new JsonObject();
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java
deleted file mode 100644 (file)
index ca0d409..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers;
-
-import java.security.KeyFactory;
-import java.security.PrivateKey;
-import java.security.spec.PKCS8EncodedKeySpec;
-
-import javax.crypto.Cipher;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO-CIPHER
- * Based on K4CZP3R's p100-java-poc
- * 
- * @author Christian Wild - Initial Initial contribution
- */
-@NonNullByDefault
-public class TapoCipher {
-    private final Logger logger = LoggerFactory.getLogger(TapoCipher.class);
-    protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
-    protected static final String CIPHER_ALGORITHM = "AES";
-    protected static final String CIPHER_CHARSET = "UTF-8";
-    protected static final String HANDSHAKE_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
-    protected static final String HANDSHAKE_ALGORITHM = "RSA";
-    protected static final String HANDSHAKE_CHARSET = "UTF-8";
-
-    @NonNullByDefault({})
-    private Cipher encodeCipher;
-    @NonNullByDefault({})
-    private Cipher decodeCipher;
-    @NonNullByDefault({})
-    private MimeEncode mimeEncode;
-
-    /**
-     * CREATE NEW EMPTY CIPHER
-     */
-    public TapoCipher() {
-    }
-
-    /**
-     * CREATE NEW CIPHER WITH KEY AND CREDENTIALS
-     * 
-     * @param handshakeKey Key from Handshake-Request
-     * @param credentials TapoCredentials
-     */
-    public TapoCipher(String handshakeKey, TapoCredentials credentials) {
-        setKey(handshakeKey, credentials);
-    }
-
-    /**
-     * SET NEW KEY AND CREDENTIALS
-     * 
-     * @param handshakeKey
-     * @param credentials
-     */
-    public void setKey(String handshakeKey, TapoCredentials credentials) {
-        logger.trace("Init TapoCipher with key: {} ", handshakeKey);
-        MimeEncode mimeEncode = new MimeEncode();
-        try {
-            byte[] decode = mimeEncode.decode(handshakeKey.getBytes(HANDSHAKE_CHARSET));
-            byte[] decode2 = mimeEncode.decode(credentials.getPrivateKeyBytes());
-            Cipher instance = Cipher.getInstance(HANDSHAKE_TRANSFORMATION);
-            KeyFactory kf = KeyFactory.getInstance(HANDSHAKE_ALGORITHM);
-            PrivateKey p = kf.generatePrivate(new PKCS8EncodedKeySpec(decode2));
-            instance.init(Cipher.DECRYPT_MODE, p);
-            byte[] doFinal = instance.doFinal(decode);
-            byte[] bArr = new byte[16];
-            byte[] bArr2 = new byte[16];
-            System.arraycopy(doFinal, 0, bArr, 0, 16);
-            System.arraycopy(doFinal, 16, bArr2, 0, 16);
-            initCipher(bArr, bArr2);
-        } catch (Exception ex) {
-            logger.warn("Something went wrong: {}", ex.getMessage());
-        }
-    }
-
-    /**
-     * INIT ENCODE/DECDE-CIPHERS
-     * 
-     * @param bArr
-     * @param bArr2
-     * @throws Exception
-     */
-    protected void initCipher(byte[] bArr, byte[] bArr2) throws Exception {
-        try {
-            mimeEncode = new MimeEncode();
-            SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, CIPHER_ALGORITHM);
-            IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
-            this.encodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
-            this.decodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
-            this.encodeCipher.init(1, secretKeySpec, ivParameterSpec);
-            this.decodeCipher.init(2, secretKeySpec, ivParameterSpec);
-        } catch (Exception e) {
-            logger.warn("initChiper failed: {}", e.getMessage());
-            this.encodeCipher = null;
-            this.decodeCipher = null;
-        }
-    }
-
-    /**
-     * ENCODE STRING
-     * 
-     * @param str source string to encode
-     * @return encoded string
-     * @throws Exception
-     */
-    public String encode(String str) throws Exception {
-        byte[] doFinal;
-        doFinal = this.encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET));
-        String encrypted = mimeEncode.encodeToString(doFinal);
-        return encrypted.replace("\r\n", "");
-    }
-
-    /**
-     * DECODE STRING
-     * 
-     * @param str source string to decode
-     * @return decoded string
-     * @throws Exception
-     */
-    public String decode(String str) throws Exception {
-        byte[] data = mimeEncode.decode(str.getBytes(CIPHER_CHARSET));
-        byte[] doFinal;
-        doFinal = this.decodeCipher.doFinal(data);
-        return new String(doFinal);
-    }
-}
index 2ccf434f622d6754978b26c9fe09241b69590273..f99f054947649ed654171558cef5cbc133558d1c 100644 (file)
  */
 package org.openhab.binding.tapocontrol.internal.helpers;
 
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.interfaces.RSAPublicKey;
-
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Handler class for TAPO Credentials
@@ -30,191 +20,13 @@ import org.slf4j.LoggerFactory;
  * @author Christian Wild - Initial contribution
  */
 @NonNullByDefault
-public class TapoCredentials {
-
-    private final Logger logger = LoggerFactory.getLogger(TapoCredentials.class);
-    private MimeEncode mimeEncoder;
-    private String encodedPassword = "";
-    private String encodedEmail = "";
-    private String publicKey = "";
-    private String privateKey = "";
-    private String username = "";
-    private String password = "";
+public record TapoCredentials(String username, String password) {
 
-    /**
-     * INIT CLASS
-     *
-     */
     public TapoCredentials() {
-        this.mimeEncoder = new MimeEncode();
-    }
-
-    /**
-     * INIT CLASS
-     *
-     * @param eMail E-Mail-adress of Tapo Cloud
-     * @param password Password of Tapo Cloud
-     */
-    public TapoCredentials(String eMail, String password) {
-        this.mimeEncoder = new MimeEncode();
-        setCredectials(eMail, password);
-    }
-
-    /**
-     * set credentials.
-     *
-     * @param eMail username (eMail-adress) of Tapo Cloud
-     * @param password Password of Tapo Cloud
-     */
-    public void setCredectials(String eMail, String password) {
-        try {
-            this.username = eMail;
-            this.password = password;
-            encryptCredentials(eMail, password);
-            createKeyPair();
-        } catch (Exception e) {
-            logger.warn("error init credential class '{}'", e.toString());
-        }
-    }
-
-    /**
-     * encrypt credentials.
-     *
-     * @param username username (eMail-adress) of Tapo Cloud
-     * @param passowrd Password of Tapo Cloud
-     */
-    private void encryptCredentials(String username, String password) throws Exception {
-        logger.trace("encrypt credentials for '{}'", username);
-
-        /* Password Encoding */
-        byte[] byteWord = password.getBytes();
-        this.encodedPassword = mimeEncoder.encodeToString(byteWord);
-
-        /* User Encoding */
-        String encodedUser = this.shaDigestUsername(username);
-        byteWord = encodedUser.getBytes("UTF-8");
-        this.encodedEmail = mimeEncoder.encodeToString(byteWord);
-    }
-
-    /**
-     * Create Key-Pairs
-     *
-     */
-    public void createKeyPair() throws NoSuchAlgorithmException {
-        logger.trace("generating new keypair");
-        KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
-        instance.initialize(1024, new SecureRandom());
-        KeyPair generateKeyPair = instance.generateKeyPair();
-
-        this.publicKey = new String(mimeEncoder.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
-        this.privateKey = new String(mimeEncoder.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
-        logger.trace("new privateKey: '{}'", this.privateKey);
-        logger.trace("new ublicKey: '{}'", this.publicKey);
-    }
-
-    /**
-     * shaDigest USERNAME
-     *
-     */
-    private String shaDigestUsername(String str) throws NoSuchAlgorithmException {
-        byte[] bArr = str.getBytes();
-        byte[] digest = MessageDigest.getInstance("SHA1").digest(bArr);
-
-        StringBuilder sb = new StringBuilder();
-        for (byte b : digest) {
-            String hexString = Integer.toHexString(b & 255);
-            if (hexString.length() == 1) {
-                sb.append("0");
-                sb.append(hexString);
-            } else {
-                sb.append(hexString);
-            }
-        }
-        return sb.toString();
-    }
-
-    /**
-     * RETURN ENCODED PASSWORD
-     *
-     */
-    public String getEncodedPassword() {
-        return encodedPassword;
-    }
-
-    /**
-     * RETURN ENCODED E-MAIL
-     *
-     */
-    public String getEncodedEmail() {
-        return encodedEmail;
-    }
-
-    /**
-     * RETURN PASSWORD
-     *
-     */
-    public String getPassword() {
-        return password;
-    }
-
-    /**
-     * RETURN Username (E-MAIL)
-     *
-     */
-    public String getUsername() {
-        return username;
-    }
-
-    /**
-     * RETURN PRIVATE-KEY
-     * 
-     * @return String -----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----
-     */
-    public String getPrivateKey() {
-        return String.format("-----BEGIN PRIVATE KEY-----%n%s%n-----END PRIVATE KEY-----%n", privateKey);
-    }
-
-    /**
-     * RETURN PUBLIC KEY
-     * 
-     * @return String -----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----
-     */
-    public String getPublicKey() {
-        return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
-    }
-
-    /**
-     * RETURN PRIVATE-KEY (BYTES)
-     * 
-     * @return UTF-8 coded byte[] with private key
-     */
-    public byte[] getPrivateKeyBytes() {
-        try {
-            return privateKey.getBytes("UTF-8");
-        } catch (Exception e) {
-            return new byte[0];
-        }
-    }
-
-    /**
-     * RETURN PUBLIC-KEY (BYTES)
-     * 
-     * @return UTF-8 coded byte[] with private key
-     */
-    public byte[] getPublicKeyBytes() {
-        try {
-            return publicKey.getBytes("UTF-8");
-        } catch (Exception e) {
-            return new byte[0];
-        }
+        this("", "");
     }
 
-    /**
-     * CHECK IF CREDENTIALS ARE SET
-     * 
-     * @return
-     */
-    public Boolean areSet() {
-        return !(this.username.isEmpty() || this.password.isEmpty());
+    public boolean areSet() {
+        return !username.isBlank() && !password.isBlank();
     }
 }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoEncoder.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoEncoder.java
new file mode 100644 (file)
index 0000000..8776d9f
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers;
+
+import static java.util.Base64.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.zip.CRC32;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Class for Encoding, Crypting, and Decrypting
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoEncoder {
+    private static final String SHA1_ALGORITHM = "SHA1";
+    private static final String SHA256_ALGORITHM = "SHA256";
+
+    /* b64 encode */
+    public static String b64Encode(String textToEncode) {
+        byte[] message = null;
+        message = textToEncode.getBytes(StandardCharsets.UTF_8);
+        return getMimeEncoder().encodeToString(message);
+    }
+
+    /* b64 decode */
+    public static String b64Decode(String textToDecode) {
+        byte[] decoded = getMimeDecoder().decode(textToDecode);
+        return byteArrayToString(decoded);
+    }
+
+    /**
+     * Checks if a string is encoded to Base64.
+     *
+     * @param input The string to be checked.
+     * @return Returns true if the string is Base64 encoded, false otherwise.
+     */
+    public static boolean isBase64Encoded(String input) {
+        try {
+            byte[] decodedBytes = Base64.getDecoder().decode(input.getBytes());
+            String decodedString = new String(decodedBytes);
+            String reencodedString = Base64.getEncoder().encodeToString(decodedString.getBytes());
+            return input.equals(reencodedString);
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    /* create sha1 hash (string) */
+    public static String sha1Encode(String textToEncode) throws NoSuchAlgorithmException {
+        byte[] digest = sha1Encode(textToEncode.getBytes(StandardCharsets.UTF_8));
+        return byteArrayToString(digest);
+    }
+
+    /* create sha1 hash (byte[]) */
+    public static byte[] sha1Encode(byte[] bArr) throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance(SHA1_ALGORITHM).digest(bArr);
+    }
+
+    /* create sha256 hash (string) */
+    public static String sha256Encode(String textToEncode) throws NoSuchAlgorithmException {
+        byte[] digest = textToEncode.getBytes(StandardCharsets.UTF_8);
+        return byteArrayToString(sha256Encode(digest));
+    }
+
+    /* create sha256 hash (byte[])) */
+    public static byte[] sha256Encode(byte[] bArr) throws NoSuchAlgorithmException {
+        return MessageDigest.getInstance(SHA256_ALGORITHM).digest(bArr);
+    }
+
+    /* get CRC32-Checksum from byteArray */
+    public static long crc32Checksum(byte[] bArr) {
+        CRC32 crc32 = new CRC32();
+        crc32.update(bArr);
+        return crc32.getValue();
+    }
+
+    /* convert bytearray[] into string */
+    public static String byteArrayToString(byte[] hexByte) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : hexByte) {
+            String hexString = Integer.toHexString(b & 255);
+            if (hexString.length() == 1) {
+                sb.append("0");
+                sb.append(hexString);
+            } else {
+                sb.append(hexString);
+            }
+        }
+        return sb.toString();
+    }
+}
index bfec0f5e525e05c4125fd0e12bc57561a96dee24..196b9d5bb4b3a1718e77e1919bdefdfcccb8a9ec 100644 (file)
@@ -14,12 +14,13 @@ package org.openhab.binding.tapocontrol.internal.helpers;
 
 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
 
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorType;
 
 /**
  * Class Handling TapoErrors
@@ -28,10 +29,9 @@ import com.google.gson.JsonObject;
  */
 @NonNullByDefault
 public class TapoErrorHandler extends Exception {
-    private TapoErrorCode errorCode = TapoErrorCode.NO_ERROR;
     private static final long serialVersionUID = 0L;
+    private Integer errorNumber = NO_ERROR.getCode();
     private String infoMessage = "";
-    private Gson gson = new Gson();
 
     /**
      * Constructor
@@ -55,7 +55,7 @@ public class TapoErrorHandler extends Exception {
      * @param errorCode error code (number)
      * @param infoMessage optional info-message
      */
-    public TapoErrorHandler(Integer errorCode, String infoMessage) {
+    public TapoErrorHandler(Integer errorCode, @Nullable String infoMessage) {
         raiseError(errorCode, infoMessage);
     }
 
@@ -74,7 +74,7 @@ public class TapoErrorHandler extends Exception {
      * @param ex Exception
      * @param infoMessage optional info-message
      */
-    public TapoErrorHandler(Exception ex, String infoMessage) {
+    public TapoErrorHandler(Exception ex, @Nullable String infoMessage) {
         raiseError(ex, infoMessage);
     }
 
@@ -93,7 +93,7 @@ public class TapoErrorHandler extends Exception {
      * @param errorCode error code (TapoErrorCodeEnum)
      * @param infoMessage optional info-message
      */
-    public TapoErrorHandler(TapoErrorCode errorCode, String infoMessage) {
+    public TapoErrorHandler(TapoErrorCode errorCode, @Nullable String infoMessage) {
         raiseError(errorCode, infoMessage);
     }
 
@@ -111,7 +111,7 @@ public class TapoErrorHandler extends Exception {
      */
     private String getErrorMessage(Integer errCode) {
         String key = TapoErrorCode.fromCode(errCode).name().replace("ERR_", "error-").replace("_", "-").toLowerCase();
-        return String.format("@text/%s [ \"%s\" ]", key, errCode.toString());
+        return String.format("@text/%s [ \"%s\" ]", key, errorNumber);
     }
 
     /***********************************
@@ -129,16 +129,6 @@ public class TapoErrorHandler extends Exception {
         raiseError(errorCode, "");
     }
 
-    /**
-     * Raises new error
-     * 
-     * @param errorCode error code (number)
-     * @param infoMessage optional info-message
-     */
-    public void raiseError(Integer errorCode, String infoMessage) {
-        raiseError(TapoErrorCode.fromCode(errorCode), infoMessage);
-    }
-
     /**
      * Raises new error
      * 
@@ -154,8 +144,20 @@ public class TapoErrorHandler extends Exception {
      * @param ex Exception
      * @param infoMessage optional info-message
      */
-    public void raiseError(Exception ex, String infoMessage) {
-        raiseError(TapoErrorCode.fromCode(ex.hashCode()), infoMessage);
+    public void raiseError(Exception ex, @Nullable String infoMessage) {
+        try {
+            throw ex;
+        } catch (TapoErrorHandler e) {
+            raiseError(e.getCode(), e.getMessagText());
+        } catch (TimeoutException e) {
+            raiseError(ERR_BINDING_CONNECT_TIMEOUT, infoMessage);
+        } catch (InterruptedException e) {
+            raiseError(ERR_BINDING_SEND_REQUEST, infoMessage);
+        } catch (ExecutionException e) {
+            raiseError(ERR_BINDING_SEND_REQUEST, infoMessage);
+        } catch (Exception e) {
+            raiseError(e.hashCode(), infoMessage);
+        }
     }
 
     /**
@@ -173,9 +175,23 @@ public class TapoErrorHandler extends Exception {
      * @param errorCode error code (TapoErrorCodeEnum)
      * @param infoMessage optional info-message
      */
-    public void raiseError(TapoErrorCode errorCode, String infoMessage) {
-        this.errorCode = errorCode;
-        this.infoMessage = infoMessage;
+    public void raiseError(TapoErrorCode errorCode, @Nullable String infoMessage) {
+        raiseError(errorCode.getCode(), infoMessage);
+    }
+
+    /**
+     * Raises new error
+     * 
+     * @param errorCode error code (number)
+     * @param infoMessage optional info-message
+     */
+    public void raiseError(Integer errorCode, @Nullable String infoMessage) {
+        errorNumber = errorCode;
+        if (infoMessage != null) {
+            this.infoMessage = infoMessage;
+        } else {
+            this.infoMessage = "";
+        }
     }
 
     /**
@@ -184,16 +200,16 @@ public class TapoErrorHandler extends Exception {
      * @param tapoError
      */
     public void set(TapoErrorHandler tapoError) {
-        this.errorCode = TapoErrorCode.fromCode(tapoError.getCode());
-        this.infoMessage = tapoError.getExtendedInfo();
+        errorNumber = tapoError.getCode();
+        infoMessage = tapoError.getExtendedInfo();
     }
 
     /**
      * Reset Error
      */
     public void reset() {
-        this.errorCode = NO_ERROR;
-        this.infoMessage = "";
+        errorNumber = NO_ERROR.getCode();
+        infoMessage = "";
     }
 
     /***********************************
@@ -210,7 +226,11 @@ public class TapoErrorHandler extends Exception {
     @Override
     @Nullable
     public String getMessage() {
-        return getErrorMessage(errorCode.getCode());
+        return getMessage(errorNumber);
+    }
+
+    public String getMessagText() {
+        return getMessage(errorNumber);
     }
 
     /**
@@ -229,7 +249,7 @@ public class TapoErrorHandler extends Exception {
      * @return error code (integer)
      */
     public Integer getCode() {
-        return this.errorCode.getCode();
+        return errorNumber;
     }
 
     /**
@@ -238,7 +258,7 @@ public class TapoErrorHandler extends Exception {
      * @return error extended info
      */
     public String getExtendedInfo() {
-        return this.infoMessage;
+        return infoMessage;
     }
 
     /**
@@ -247,7 +267,7 @@ public class TapoErrorHandler extends Exception {
      * @return error code
      */
     public TapoErrorCode getError() {
-        return this.errorCode;
+        return TapoErrorCode.fromCode(errorNumber);
     }
 
     /**
@@ -255,23 +275,16 @@ public class TapoErrorHandler extends Exception {
      * 
      * @return true if has error
      */
-    public Boolean hasError() {
-        return this.errorCode != NO_ERROR;
+    public boolean hasError() {
+        return !NO_ERROR.getCode().equals(errorNumber);
     }
 
-    /**
-     * Get JSON-Object with errror
-     * 
-     * @return JsonObject with error-informations
-     */
-    public JsonObject getJson() {
-        JsonObject json;
-        json = gson.fromJson(
-                "{'error_code': '" + errorCode + "', 'error_message':'" + getErrorMessage(getCode()) + "'}",
-                JsonObject.class);
-        if (json == null) {
-            json = new JsonObject();
-        }
-        return json;
+    public TapoErrorType getType() {
+        return TapoErrorCode.fromCode(errorNumber).getType();
+    }
+
+    @Override
+    public String toString() {
+        return getErrorMessage(getCode());
     }
 }
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoKeyPair.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoKeyPair.java
new file mode 100644 (file)
index 0000000..1644543
--- /dev/null
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers;
+
+import static java.util.Base64.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Tapo RSA-KeyPair Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoKeyPair {
+    private static final String ALGORITHM_RSA = "RSA";
+    private static final String LINE_SEPARATOR = "\n";
+    private static final int LINE_LENGTH = 64;
+    private byte[] publicKeyBytes = {};
+    private byte[] privateKeyBytes = {};
+
+    public TapoKeyPair(int keysize) {
+        try {
+            createNewKeyPair(ALGORITHM_RSA, keysize);
+        } catch (Exception e) {
+            publicKeyBytes = new byte[0];
+            privateKeyBytes = new byte[0];
+        }
+    }
+
+    public TapoKeyPair(String algorithm, int keysize) throws NoSuchAlgorithmException {
+        createNewKeyPair(algorithm, keysize);
+    }
+
+    /**
+     * Create Key-Pairs
+     *
+     */
+    public void createNewKeyPair(String algorithm, int keysize) throws NoSuchAlgorithmException {
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
+        keyPairGenerator.initialize(keysize, new SecureRandom());
+        KeyPair generateKeyPair = keyPairGenerator.generateKeyPair();
+        Encoder mimEncoder = getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));
+
+        publicKeyBytes = mimEncoder.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded());
+        privateKeyBytes = mimEncoder.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded());
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    /**
+     * get PEM-formated Private-Key
+     * 
+     * @return String -----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----
+     */
+    public String getPrivateKey() {
+        return String.format("-----BEGIN PRIVATE KEY-----%2$s%1$s%2$s-----END PRIVATE KEY-----%2$s",
+                new String(privateKeyBytes), LINE_SEPARATOR);
+    }
+
+    /**
+     * get PEM-formated Public-Key
+     * 
+     * @return String -----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----
+     */
+    public String getPublicKey() {
+        return String.format("-----BEGIN PUBLIC KEY-----%2$s%1$s%2$s-----END PUBLIC KEY-----%2$s",
+                new String(publicKeyBytes), LINE_SEPARATOR);
+    }
+
+    /**
+     * get Private-Key as byte-array
+     * 
+     * @return UTF-8 coded byte[] with private key
+     */
+    public byte[] getPrivateKeyBytes() {
+        return privateKeyBytes;
+    }
+
+    /**
+     * get Public-Key as byte-array
+     * 
+     * @return UTF-8 coded byte[] with private key
+     */
+    public byte[] getPublicKeyBytes() {
+        return publicKeyBytes;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoUtils.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoUtils.java
deleted file mode 100644 (file)
index 21a66ae..0000000
+++ /dev/null
@@ -1,371 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers;
-
-import javax.measure.Unit;
-import javax.measure.quantity.Energy;
-import javax.measure.quantity.Power;
-import javax.measure.quantity.Time;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.types.StringType;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-
-/**
- * {@link TapoUtils} TapoUtils -
- * Utility Helper Functions
- *
- * @author Christian Wild - Initial Initial contribution
- */
-@NonNullByDefault
-public class TapoUtils {
-
-    /************************************
-     * CALCULATION UTILS
-     ***********************************/
-    /**
-     * Limit Value between limits
-     * 
-     * @param value Integer
-     * @param lowerLimit
-     * @param upperLimit
-     * @return
-     */
-    public static Integer limitVal(@Nullable Integer value, Integer lowerLimit, Integer upperLimit) {
-        if (value == null || value < lowerLimit) {
-            return lowerLimit;
-        } else if (value > upperLimit) {
-            return upperLimit;
-        }
-        return value;
-    }
-
-    /************************************
-     * FORMAT UTILS
-     ***********************************/
-    /**
-     * return value or default val if it's null
-     * 
-     * @param <T> Type of value
-     * @param value value
-     * @param defaultValue defaut value
-     * @return
-     */
-    public static <T> T getValueOrDefault(@Nullable T value, T defaultValue) {
-        return value == null ? defaultValue : value;
-    }
-
-    /**
-     * Format MAC-Address replacing old division chars and add new one
-     * 
-     * @param mac unformated mac-Address
-     * @param newDivisionChar new division char (e.g. ":","-" )
-     * @return new formated mac-Address
-     */
-    public static String formatMac(String mac, char newDivisionChar) {
-        String unformatedMac = unformatMac(mac);
-        return unformatedMac.replaceAll("(.{2})", "$1" + newDivisionChar).substring(0, 17);
-    }
-
-    /**
-     * unformat MAC-Address replace all division chars
-     * 
-     * @param mac
-     * @return
-     */
-    public static String unformatMac(String mac) {
-        mac = mac.replace("-", "");
-        mac = mac.replace(":", "");
-        mac = mac.replace(".", "");
-        return mac;
-    }
-
-    /**
-     * HEX-STRING to byte convertion
-     */
-    public static byte[] hexStringToByteArray(String s) {
-        int len = s.length();
-        byte[] data = new byte[len / 2];
-        try {
-            for (int i = 0; i < len; i += 2) {
-                data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
-            }
-        } catch (Exception e) {
-        }
-        return data;
-    }
-
-    /**
-     * Return Boolean from string
-     * 
-     * @param s - string to be converted
-     * @param defVal - Default Value
-     */
-    public Boolean stringToBool(@Nullable String s, boolean defVal) {
-        if (s == null) {
-            return defVal;
-        }
-        try {
-            return Boolean.parseBoolean(s);
-        } catch (Exception e) {
-            return defVal;
-        }
-    }
-
-    /**
-     * Return Integer from string
-     * 
-     * @param s - string to be converted
-     * @param defVal - Default Value
-     */
-    public Integer stringToInteger(@Nullable String s, Integer defVal) {
-        if (s == null) {
-            return defVal;
-        }
-        try {
-            return Integer.valueOf(s);
-        } catch (Exception e) {
-            return defVal;
-        }
-    }
-
-    /***********************************
-     * JSON-FORMATER
-     ************************************/
-
-    public static boolean isValidJson(String json) {
-        try {
-            Gson gson = new Gson();
-            JsonObject jsnObject = gson.fromJson(json, JsonObject.class);
-            return jsnObject != null;
-        } catch (Exception e) {
-            return false;
-        }
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @param defVal - default value;
-     * @return string value
-     */
-    public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name, String defVal) {
-        if (jsonObject != null && jsonObject.has(name)) {
-            return jsonObject.get(name).getAsString();
-        } else {
-            return defVal;
-        }
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @return string value
-     */
-    public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name) {
-        return jsonObjectToString(jsonObject, name, "");
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @param defVal - default value;
-     * @return boolean value
-     */
-    public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name, Boolean defVal) {
-        if (jsonObject != null && jsonObject.has(name)) {
-            return jsonObject.get(name).getAsBoolean();
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @return boolean value
-     */
-    public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name) {
-        return jsonObjectToBool(jsonObject, name, false);
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @param defVal - default value;
-     * @return integer value
-     */
-    public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name, Integer defVal) {
-        if (jsonObject != null && jsonObject.has(name)) {
-            return jsonObject.get(name).getAsInt();
-        } else {
-            return defVal;
-        }
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @return integer value
-     */
-    public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name) {
-        return jsonObjectToInt(jsonObject, name, 0);
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @param defVal - default value;
-     * @return number value
-     */
-    public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name, Number defVal) {
-        if (jsonObject != null && jsonObject.has(name)) {
-            return jsonObject.get(name).getAsNumber();
-        } else {
-            return defVal;
-        }
-    }
-
-    /**
-     * 
-     * @param name parameter name
-     * @return number value
-     */
-    public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name) {
-        return jsonObjectToNumber(jsonObject, name, 0);
-    }
-
-    /************************************
-     * TYPE UTILS
-     ***********************************/
-
-    /**
-     * Return OnOffType from bool
-     * 
-     * @param boolVal
-     */
-    public static OnOffType getOnOffType(@Nullable Boolean boolVal) {
-        return boolVal != null ? OnOffType.from(boolVal) : OnOffType.OFF;
-    }
-
-    /**
-     * Return OnOffType from bool
-     * 
-     * @param intVal
-     */
-    public static OnOffType getOnOffType(Integer intVal) {
-        return OnOffType.from(intVal != 0);
-    }
-
-    /**
-     * Return StringType from String
-     * 
-     * @param strVal
-     */
-    public static StringType getStringType(@Nullable String strVal) {
-        return new StringType(strVal != null ? strVal : "");
-    }
-
-    /**
-     * Return DecimalType from Double
-     * 
-     * @param numVal
-     */
-    public static DecimalType getDecimalType(@Nullable Double numVal) {
-        return new DecimalType((numVal != null ? numVal : 0));
-    }
-
-    /**
-     * Return DecimalType from Integer
-     * 
-     * @param numVal
-     */
-    public static DecimalType getDecimalType(@Nullable Integer numVal) {
-        return new DecimalType((numVal != null ? numVal : 0));
-    }
-
-    /**
-     * Return DecimalType from Long
-     * 
-     * @param numVal
-     */
-    public static DecimalType getDecimalTypel(@Nullable Long numVal) {
-        return new DecimalType((numVal != null ? numVal : 0));
-    }
-
-    /**
-     * 
-     * @param numVal value 0-100
-     * @return PercentType
-     */
-    public static PercentType getPercentType(@Nullable Integer numVal) {
-        Integer val = limitVal(numVal, 0, 100);
-        return new PercentType(val);
-    }
-
-    /**
-     * Return HSBType from integers
-     * 
-     * @param hue integer hue-color
-     * @param saturation integer saturation
-     * @param brightness integer brightness
-     * @return HSBType
-     */
-    public static HSBType getHSBType(Integer hue, Integer saturation, Integer brightness) {
-        DecimalType h = new DecimalType(hue);
-        PercentType s = new PercentType(saturation);
-        PercentType b = new PercentType(brightness);
-        return new HSBType(h, s, b);
-    }
-
-    /**
-     * Return QuantityType with Time
-     * 
-     * @param numVal Number with value
-     * @param unit TimeUnit ({@code Unit<Time>})
-     * @return {@code QuantityType<Time>}
-     */
-    public static QuantityType<Time> getTimeType(@Nullable Number numVal, Unit<Time> unit) {
-        return new QuantityType<>((numVal != null ? numVal : 0), unit);
-    }
-
-    /**
-     * Return QuantityType with Power
-     * 
-     * @param numVal Number with value
-     * @param unit PowerUnit ({@code Unit<Power>})
-     * @return {@code QuantityType<Power>}
-     */
-    public static QuantityType<Power> getPowerType(@Nullable Number numVal, Unit<Power> unit) {
-        return new QuantityType<>((numVal != null ? numVal : 0), unit);
-    }
-
-    /**
-     * Return QuantityType with Energy
-     * 
-     * @param numVal Number with value
-     * @param unit PowerUnit ({@code Unit<Power>})
-     * @return {@code QuantityType<Energy>}
-     */
-    public static QuantityType<Energy> getEnergyType(@Nullable Number numVal, Unit<Energy> unit) {
-        return new QuantityType<>((numVal != null ? numVal : 0), unit);
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/ByteUtils.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/ByteUtils.java
new file mode 100644 (file)
index 0000000..a5875b6
--- /dev/null
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * {@link ByteUtils} ByteUtils -
+ * Utility Helper Functions handling byte helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class ByteUtils {
+
+    /**
+     * Truncate Byte Array
+     * 
+     * @param bytes full byteArray
+     * @param srcPos startindex
+     * @param newLength new size of byte array
+     * @return truncated byte array
+     */
+    public static byte[] truncateByteArray(byte[] bytes, int srcPos, int newLength) {
+        if (bytes.length < newLength) {
+            return bytes;
+        } else {
+            byte[] truncated = new byte[newLength];
+            System.arraycopy(bytes, srcPos, truncated, 0, newLength);
+            return truncated;
+        }
+    }
+
+    /**
+     * Concat Byte Arrays
+     * 
+     * @param bytes bytes to concat as array
+     * @return byte array with concated input bytes
+     * @throws TapoErrorHandler
+     */
+    public static byte[] concatBytes(byte[]... bytes) throws TapoErrorHandler {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            for (byte[] b : bytes) {
+                outputStream.write(b);
+            }
+            return outputStream.toByteArray();
+        } catch (Exception e) {
+            throw new TapoErrorHandler(ERR_DATA_TRANSORMATION);
+        }
+    }
+
+    /**
+     * Replace bytes in bytearray
+     * 
+     * @param oldBytes original byte array
+     * @param replace replacing byte array
+     * @param startPos position bytes should be replaced
+     * @return array of bytes with replaced data
+     */
+    public static byte[] replaceBytes(byte[] oldBytes, byte[] replace, int startPos) {
+        System.arraycopy(replace, 0, oldBytes, startPos, replace.length);
+        return oldBytes;
+    }
+
+    /**
+     * HEX-STRING to byte convertion
+     * 
+     * @param s hex-formated string
+     * @return array of bytes from hex-string
+     */
+    public static byte[] hexStringToByteArray(String s) {
+        int len = s.length();
+        byte[] data = new byte[len / 2];
+        try {
+            for (int i = 0; i < len; i += 2) {
+                data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
+            }
+        } catch (Exception e) {
+        }
+        return data;
+    }
+
+    /**
+     * Convert byte to hex-string
+     * 
+     * @param num single byte
+     * @return hex-string
+     */
+    public static String byteToHex(byte num) {
+        char[] hexDigits = new char[2];
+        hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
+        hexDigits[1] = Character.forDigit((num & 0xF), 16);
+        return new String(hexDigits);
+    }
+
+    /**
+     * Convert ByteArray to hex-string
+     * 
+     * @param byteArray array of bytes to convert
+     * @return hex-string
+     */
+    public static String byteArrayToHex(byte[] byteArray) {
+        StringBuffer hexStringBuffer = new StringBuffer();
+        for (int i = 0; i < byteArray.length; i++) {
+            hexStringBuffer.append(byteToHex(byteArray[i]));
+        }
+        return hexStringBuffer.toString();
+    }
+
+    /**
+     * Convert byteArray to int
+     * 
+     * @param bArr array of bytes
+     * @param byteOrder BigEndian or LittleEndian
+     * @return int value of bytebuffer
+     */
+    public static int byteArrayToInt(byte[] bArr, ByteOrder byteOrder) {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+        byteBuffer.order(byteOrder);
+        byteBuffer.put(bArr);
+        return byteBuffer.getInt();
+    }
+
+    /**
+     * Convert byteArray to short
+     * 
+     * @param bArr array of bytes
+     * @param byteOrder BigEndian or LittleEndian
+     * @return short value of bytebuffer
+     */
+    public static short byteArrayToShort(byte[] bArr, ByteOrder byteOrder) {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+        byteBuffer.order(byteOrder);
+        byteBuffer.put(bArr);
+        return byteBuffer.getShort();
+    }
+
+    /**
+     * Convert int into byteArray
+     * 
+     * @param i integer value
+     * @param byteOrder BigEndian or LittleEndian
+     * @return array of bytes
+     */
+    public static byte[] intToByteArray(int i, ByteOrder byteOrder) {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+        byteBuffer.order(byteOrder);
+        byteBuffer.putInt(i);
+        return byteBuffer.array();
+    }
+
+    /**
+     * Convert short to byteArray
+     * 
+     * @param s short value
+     * @param byteOrder BigEndian or LittleEndian
+     * @return array of bytes
+     */
+    public static byte[] shortToByteArray(short s, ByteOrder byteOrder) {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(2);
+        byteBuffer.order(byteOrder);
+        byteBuffer.putShort(s);
+        return byteBuffer.array();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/JsonUtils.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/JsonUtils.java
new file mode 100644 (file)
index 0000000..937e101
--- /dev/null
@@ -0,0 +1,179 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * {@link JsonUtils} JsonUtils -
+ * Utility Helper Functions handling json helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class JsonUtils {
+
+    /**
+     * Check if string is valid json
+     * 
+     * @param json string to check
+     * @return true if is valid json
+     */
+    public static boolean isValidJson(String json) {
+        try {
+            JsonObject jsnObject = GSON.fromJson(json, JsonObject.class);
+            return jsnObject != null;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * Get String from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name (json-key)
+     * @param defVal default value;
+     * @return string value
+     */
+    public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name, String defVal) {
+        if (jsonObject != null && jsonObject.has(name)) {
+            return jsonObject.get(name).getAsString();
+        } else {
+            return defVal;
+        }
+    }
+
+    /**
+     * Get String from jsonObject
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name
+     * @return string value
+     */
+    public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name) {
+        return jsonObjectToString(jsonObject, name, "");
+    }
+
+    /**
+     * Get Boolean from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name (json-key)
+     * @param defVal - default value;
+     * @return boolean value
+     */
+    public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name, Boolean defVal) {
+        if (jsonObject != null && jsonObject.has(name)) {
+            return jsonObject.get(name).getAsBoolean();
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Get Boolean from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name (json-key)
+     * @return boolean value
+     */
+    public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name) {
+        return jsonObjectToBool(jsonObject, name, false);
+    }
+
+    /**
+     * Get Integer from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name (json-key)
+     * @param defVal - default value;
+     * @return integer value
+     */
+    public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name, Integer defVal) {
+        if (jsonObject != null && jsonObject.has(name)) {
+            return jsonObject.get(name).getAsInt();
+        } else {
+            return defVal;
+        }
+    }
+
+    /**
+     * Get Integer from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name (json-key)
+     * @return integer value
+     */
+    public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name) {
+        return jsonObjectToInt(jsonObject, name, 0);
+    }
+
+    /**
+     * Get Number from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name
+     * @param defVal - default value;
+     * @return number value
+     */
+    public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name, Number defVal) {
+        if (jsonObject != null && jsonObject.has(name)) {
+            return jsonObject.get(name).getAsNumber();
+        } else {
+            return defVal;
+        }
+    }
+
+    /**
+     * Get Number from jsonObject with a json key
+     * 
+     * @param jsonObject jsonObject
+     * @param name parameter name
+     * @return number value
+     */
+    public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name) {
+        return jsonObjectToNumber(jsonObject, name, 0);
+    }
+
+    /**
+     * Return class object from json formated string
+     * 
+     * @param json json formatted string
+     * @param clazz class string should parsed to
+     */
+    public static <T> T getObjectFromJson(String json, Class<T> clazz) throws JsonParseException {
+        @Nullable
+        T result = GSON.fromJson(json, clazz);
+        if (result == null) {
+            throw new JsonParseException("result is null");
+        }
+        return result;
+    }
+
+    /**
+     * Return class object from JsonObject
+     * 
+     * @param jso JsonOject
+     * @param clazz class string should parsed to
+     */
+    public static <T> T getObjectFromJson(JsonObject jso, Class<T> clazz) throws JsonParseException {
+        return getObjectFromJson(jso.toString(), clazz);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/StringUtils.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/StringUtils.java
new file mode 100644 (file)
index 0000000..509d321
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link StringUtils} StringUtils -
+ * Utility Helper Functions handling String helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class StringUtils {
+    /**
+     * Return Boolean from string
+     * 
+     * @param s - string to be converted
+     * @param defVal - Default Value
+     * @return boolean value
+     */
+    public static Boolean stringToBool(@Nullable String s, boolean defVal) {
+        if (s == null) {
+            return defVal;
+        }
+        try {
+            return Boolean.parseBoolean(s);
+        } catch (Exception e) {
+            return defVal;
+        }
+    }
+
+    /**
+     * Return Integer from string
+     * 
+     * @param s - string to be converted
+     * @param defVal - Default Value
+     * @return Integer
+     */
+    public static Integer stringToInteger(@Nullable String s, Integer defVal) {
+        if (s == null) {
+            return defVal;
+        }
+        try {
+            return Integer.valueOf(s);
+        } catch (Exception e) {
+            return defVal;
+        }
+    }
+
+    /**
+     * Get String from object
+     * 
+     * @param o - object to be converted
+     * @param defVal - Default value
+     * @return String
+     */
+    public static String objectToString(@Nullable Object o, String defVal) {
+        if (o == null) {
+            return defVal;
+        }
+        try {
+            return o.toString();
+        } catch (Exception e) {
+            return defVal;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TapoUtils.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TapoUtils.java
new file mode 100644 (file)
index 0000000..604c175
--- /dev/null
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResult;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * {@link TapoUtils} TapoUtils -
+ * Utility Helper Functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class TapoUtils {
+
+    /************************************
+     * CALCULATION UTILS
+     ***********************************/
+    /**
+     * Limit Value between limits
+     * 
+     * @param value Integer
+     * @param lowerLimit
+     * @param upperLimit
+     * @return
+     */
+    public static Integer limitVal(@Nullable Integer value, Integer lowerLimit, Integer upperLimit) {
+        if (value == null || value < lowerLimit) {
+            return lowerLimit;
+        } else if (value > upperLimit) {
+            return upperLimit;
+        }
+        return value;
+    }
+
+    /************************************
+     * FORMAT UTILS
+     ***********************************/
+    /**
+     * return value or default val if it's null
+     * 
+     * @param <T> Type of value
+     * @param value value
+     * @param defaultValue defaut value
+     * @return
+     */
+    public static <T> T getValueOrDefault(@Nullable T value, T defaultValue) {
+        return value == null ? defaultValue : value;
+    }
+
+    /**
+     * compare tow values against an comparator and return the other one
+     * if both are null, comparator will be returned - if both have values val2 will be returned
+     * 
+     * @param <T> Type of return value
+     * @param val1 fist value to campare - will be returned if val2 is null or matches comparator
+     * @param val2 second value to compare - will be returned if val1 is null or matches comparator
+     * @param comparator compared values with this
+     * @return
+     */
+    public static <T> T compareValuesAgainstComparator(@Nullable T val1, @Nullable T val2, T comparator) {
+        if (val1 == null && val2 == null) {
+            return comparator;
+        } else if (val1 != null && (val2 == null || val2.equals(comparator))) {
+            return Objects.requireNonNull(val1);
+        } else if (val1 == null || val1.equals(comparator)) {
+            return Objects.requireNonNull(val2);
+        } else {
+            return Objects.requireNonNull(val2);
+        }
+    }
+
+    /**
+     * Format MAC-Address replacing old division chars and add new one
+     * 
+     * @param mac unformated mac-Address
+     * @param newDivisionChar new division char (e.g. ":","-" )
+     * @return new formated mac-Address
+     */
+    public static String formatMac(String mac, char newDivisionChar) {
+        String unformatedMac = unformatMac(mac);
+        return unformatedMac.replaceAll("(.{2})", "$1" + newDivisionChar).substring(0, 17);
+    }
+
+    /**
+     * unformat MAC-Address replace all division chars
+     * 
+     * @param mac string with mac address
+     * @return mac address without any division chars
+     */
+    public static String unformatMac(String mac) {
+        mac = mac.replace("-", "");
+        mac = mac.replace(":", "");
+        mac = mac.replace(".", "");
+        return mac;
+    }
+
+    /**
+     * Get DeviceModel from String - Formats different spellings in model-strings
+     * 
+     * @param device JsonObject with deviceData
+     * @return String with DeviceModel
+     */
+    public static String getDeviceModel(TapoDiscoveryResult device) {
+        return getDeviceModel(device.deviceModel());
+    }
+
+    /**
+     * Get DeviceModel from String - Formats different spellings in model-strings
+     * 
+     * @param deviceModel String to find model from
+     * @return String with DeviceModel
+     */
+    public static String getDeviceModel(String deviceModel) {
+        try {
+            deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
+            deviceModel = deviceModel.replace("Tapo", "");
+            deviceModel = deviceModel.replace("Series", "");
+            deviceModel = deviceModel.trim();
+            deviceModel = deviceModel.replace(" ", "_");
+            deviceModel = deviceModel.substring(0, 4);
+            return deviceModel;
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    /**
+     * GET DEVICE LABEL
+     * 
+     * @param device JsonObject with deviceData
+     * @return String with DeviceLabel
+     */
+    public static String getDeviceLabel(TapoDiscoveryResult device) {
+        try {
+            String deviceLabel = "";
+            String deviceModel = getDeviceModel(device);
+            String alias = device.alias();
+            ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+            if (SUPPORTED_HUB_UIDS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_HUB;
+            } else if (SUPPORTED_SOCKET_UIDS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_SOCKET;
+            } else if (SUPPORTED_SOCKET_STRIP_UIDS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_SOCKET_STRIP;
+            } else if (SUPPORTED_WHITE_BULB_UIDS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_WHITE_BULB;
+            } else if (SUPPORTED_COLOR_BULB_UIDS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_COLOR_BULB;
+            } else if (SUPPORTED_LIGHT_STRIP_UIDS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_LIGHTSTRIP;
+            } else if (SUPPORTED_SMART_CONTACTS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_SMART_CONTACT;
+            } else if (SUPPORTED_MOTION_SENSORS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_MOTION_SENSOR;
+            } else if (SUPPORTED_WHEATHER_SENSORS.contains(deviceUID)) {
+                deviceLabel = DEVICE_DESCRIPTION_TEMP_SENSOR;
+            }
+            if (alias.length() > 0) {
+                return String.format("%s %s %s (%s)", DEVICE_VENDOR, deviceModel, deviceLabel, alias);
+            }
+            return String.format("%s %s %s", DEVICE_VENDOR, deviceModel, deviceLabel);
+        } catch (Exception e) {
+            return "";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TypeUtils.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TypeUtils.java
new file mode 100644 (file)
index 0000000..3ee6177
--- /dev/null
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) 2010-2024 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.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+import javax.measure.quantity.Temperature;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+
+/**
+ * {@link TypeUtils} TypeUtils -
+ * Utility Helper Functions handling type helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class TypeUtils {
+    /**
+     * Return OnOffType from bool
+     * 
+     * @param boolVal
+     */
+    public static OnOffType getOnOffType(@Nullable Boolean boolVal) {
+        return boolVal != null ? OnOffType.from(boolVal) : OnOffType.OFF;
+    }
+
+    /**
+     * Return OnOffType from Integer
+     * 
+     * @param intVal
+     */
+    public static OnOffType getOnOffType(Integer intVal) {
+        return OnOffType.from(intVal != 0);
+    }
+
+    /**
+     * Return StringType from String
+     * 
+     * @param strVal
+     */
+    public static StringType getStringType(@Nullable String strVal) {
+        return new StringType(strVal != null ? strVal : "");
+    }
+
+    /**
+     * Return DecimalType from Double
+     * 
+     * @param numVal
+     */
+    public static DecimalType getDecimalType(@Nullable Double numVal) {
+        return new DecimalType((numVal != null ? numVal : 0));
+    }
+
+    /**
+     * Return DecimalType from Integer
+     * 
+     * @param numVal
+     */
+    public static DecimalType getDecimalType(@Nullable Integer numVal) {
+        return new DecimalType((numVal != null ? numVal : 0));
+    }
+
+    /**
+     * Return DecimalType from Long
+     * 
+     * @param numVal
+     */
+    public static DecimalType getDecimalTypel(@Nullable Long numVal) {
+        return new DecimalType((numVal != null ? numVal : 0));
+    }
+
+    /**
+     * 
+     * @param numVal value 0-100
+     * @return PercentType
+     */
+    public static PercentType getPercentType(@Nullable Integer numVal) {
+        Integer val = limitVal(numVal, 0, 100);
+        return new PercentType(val);
+    }
+
+    /**
+     * Return HSBType from integers
+     * 
+     * @param hue integer hue-color
+     * @param saturation integer saturation
+     * @param brightness integer brightness
+     * @return HSBType
+     */
+    public static HSBType getHSBType(Integer hue, Integer saturation, Integer brightness) {
+        DecimalType h = new DecimalType(hue);
+        PercentType s = new PercentType(saturation);
+        PercentType b = new PercentType(brightness);
+        return new HSBType(h, s, b);
+    }
+
+    /**
+     * Return QuantityType with Time
+     * 
+     * @param numVal Number with value
+     * @param unit TimeUnit
+     * @return QuantityType of Type Time
+     */
+    public static QuantityType<Time> getTimeType(@Nullable Number numVal, Unit<Time> unit) {
+        return new QuantityType<>((numVal != null ? numVal : 0), unit);
+    }
+
+    /**
+     * Return QuantityType with Power
+     * 
+     * @param numVal Number with value
+     * @param unit PowerUnit
+     * @return QuantityType of Type Power
+     */
+    public static QuantityType<Power> getPowerType(@Nullable Number numVal, Unit<Power> unit) {
+        return new QuantityType<>((numVal != null ? numVal : 0), unit);
+    }
+
+    /**
+     * Return QuantityType with Energy
+     * 
+     * @param numVal Number with value
+     * @param unit PowerUnit
+     * @return QuantityType of Type Energy
+     */
+    public static QuantityType<Energy> getEnergyType(@Nullable Number numVal, Unit<Energy> unit) {
+        return new QuantityType<>((numVal != null ? numVal : 0), unit);
+    }
+
+    /**
+     * Return QuantityType with Temperature
+     * 
+     * @param numVal Number with value
+     * @param unit TemperatureUnit
+     * @return QuantityType of Type Temperature
+     */
+    public static QuantityType<Temperature> getTemperatureType(@Nullable Number numVal, Unit<Temperature> unit) {
+        return new QuantityType<>((numVal != null ? numVal : 0), unit);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java
deleted file mode 100644 (file)
index 3ded512..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
- *
- * @author Christian Wild - Initial contribution
- */
-
-@NonNullByDefault
-public final class TapoBridgeConfiguration {
-    /* THING CONFIGUTATION PROPERTYS */
-    public static final String CONFIG_EMAIL = "username";
-    public static final String CONFIG_PASS = "password";
-    public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
-    public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
-
-    /* DEFAULT & FIXED CONFIGURATIONS */
-    public static final Integer CONFIG_CLOUD_FIXED_INTERVAL = 1440;
-
-    /* thing configuration parameter. */
-    public String username = "";
-    public String password = "";
-    public boolean cloudDiscovery = false;
-    public int reconnectInterval = CONFIG_CLOUD_FIXED_INTERVAL;
-    public int discoveryInterval = 60;
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChild.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChild.java
deleted file mode 100644 (file)
index 271e3b7..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.MAC_DIVISION_CHAR;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.formatMac;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Tapo Child Device Information class
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class TapoChild {
-    private String fwVer = "";
-    private String hwVer = "";
-    private String type = "";
-    private String model = "";
-    private String mac = "";
-    private String category = "";
-    private String deviceId = "";
-    private boolean overheatStatus = false;
-    private int bindCount = 0;
-    private long onTime = 0;
-    private int slotNumber = 0;
-    private int position = 0;
-    private String nickname = "";
-    private boolean deviceOn = false;
-    private String region = "";
-
-    /***********************************
-     *
-     * GET VALUES
-     *
-     ************************************/
-
-    public String getFirmwareVersion() {
-        return fwVer;
-    }
-
-    public String getHardwareVersion() {
-        return hwVer;
-    }
-
-    public Boolean isOff() {
-        return !deviceOn;
-    }
-
-    public Boolean isOn() {
-        return deviceOn;
-    }
-
-    public String getMAC() {
-        return formatMac(mac, MAC_DIVISION_CHAR);
-    }
-
-    public String getModel() {
-        return model.replace(" Series", "");
-    }
-
-    public String getNickname() {
-        return nickname;
-    }
-
-    public Number getOnTime() {
-        return onTime;
-    }
-
-    public String getRegion() {
-        return region;
-    }
-
-    public String getRepresentationProperty() {
-        return getMAC();
-    }
-
-    public String getSerial() {
-        return deviceId;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    public String getFwVer() {
-        return fwVer;
-    }
-
-    public String getHwVer() {
-        return hwVer;
-    }
-
-    public String getMac() {
-        return mac;
-    }
-
-    public String getCategory() {
-        return category;
-    }
-
-    public String getDeviceId() {
-        return deviceId;
-    }
-
-    public Boolean getOverheatStatus() {
-        return overheatStatus;
-    }
-
-    public Integer getBindCount() {
-        return bindCount;
-    }
-
-    public Integer getSlotNumber() {
-        return slotNumber;
-    }
-
-    public Integer getPosition() {
-        return position;
-    }
-
-    public Boolean getDeviceOn() {
-        return deviceOn;
-    }
-
-    public void setDeviceOn(Boolean deviceOn) {
-        this.deviceOn = deviceOn;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChildData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChildData.java
deleted file mode 100644 (file)
index 3cd05f3..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Tapo-Child Structure Class
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class TapoChildData {
-    private int startIndex = 0;
-    private int sum = 0;
-    private List<TapoChild> childDeviceList = List.of();
-
-    public int getStartIndex() {
-        return startIndex;
-    }
-
-    public int getSum() {
-        return sum;
-    }
-
-    public List<TapoChild> getChildDeviceList() {
-        return childDeviceList;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java
deleted file mode 100644 (file)
index df9ce09..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters.
- *
- * @author Christian Wild - Initial contribution
- */
-
-@NonNullByDefault
-public final class TapoDeviceConfiguration {
-    /* THING CONFIGUTATION PROPERTYS */
-    public static final String CONFIG_DEVICE_IP = "ipAddress";
-    public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
-
-    /* thing configuration parameter. */
-    public String ipAddress = "";
-    public int pollingInterval = 30;
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java
deleted file mode 100644 (file)
index 5dbb2ac..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.PercentType;
-
-import com.google.gson.JsonObject;
-
-/**
- * Tapo-Device Information class
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoDeviceInfo {
-    private Boolean deviceOn = false;
-    private Boolean overheated = false;
-    private Integer brightness = 0;
-    private Integer colorTemp = 0;
-    private Integer hue = 0;
-    private Integer rssi = 0;
-    private Integer saturation = 100;
-    private Integer signalLevel = 0;
-    private Number onTime = 0;
-    private Number timeUsagePast7 = 0;
-    private Number timeUsagePast30 = 0;
-    private Number timeUsageToday = 0;
-    private String deviceId = "";
-    private String fwVer = "";
-    private String hwVer = "";
-    private String ip = "";
-    private String mac = "";
-    private String model = "";
-    private String nickname = "";
-    private String region = "";
-    private String type = "";
-    private TapoLightEffect lightEffect = new TapoLightEffect();
-
-    private JsonObject jsonObject = new JsonObject();
-
-    /**
-     * INIT
-     */
-    public TapoDeviceInfo() {
-        setData();
-    }
-
-    /**
-     * Init DeviceInfo with new Data;
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoDeviceInfo(JsonObject jso) {
-        jsonObject = jso;
-        setData();
-    }
-
-    /**
-     * Set Data (new JsonObject)
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoDeviceInfo setData(JsonObject jso) {
-        this.jsonObject = jso;
-        setData();
-        return this;
-    }
-
-    private void setData() {
-        this.brightness = jsonObjectToInt(jsonObject, JSON_KEY_BRIGHTNESS);
-        this.colorTemp = jsonObjectToInt(jsonObject, JSON_KEY_COLORTEMP, BULB_MIN_COLORTEMP);
-        this.deviceId = jsonObjectToString(jsonObject, JSON_KEY_ID);
-        this.deviceOn = jsonObjectToBool(jsonObject, JSON_KEY_ON);
-        this.fwVer = jsonObjectToString(jsonObject, JSON_KEY_FW);
-        this.hue = jsonObjectToInt(jsonObject, JSON_KEY_HUE);
-        this.hwVer = jsonObjectToString(jsonObject, JSON_KEY_HW_VER);
-        this.ip = jsonObjectToString(jsonObject, JSON_KEY_IP);
-        this.mac = jsonObjectToString(jsonObject, JSON_KEY_MAC);
-        this.model = jsonObjectToString(jsonObject, JSON_KEY_MODEL);
-        this.nickname = jsonObjectToString(jsonObject, JSON_KEY_NICKNAME);
-        this.onTime = jsonObjectToNumber(jsonObject, JSON_KEY_ONTIME);
-        this.overheated = jsonObjectToBool(jsonObject, JSON_KEY_OVERHEAT);
-        this.region = jsonObjectToString(jsonObject, JSON_KEY_REGION);
-        this.saturation = jsonObjectToInt(jsonObject, JSON_KEY_SATURATION);
-        this.signalLevel = jsonObjectToInt(jsonObject, JSON_KEY_SIGNAL_LEVEL);
-        this.rssi = jsonObjectToInt(jsonObject, JSON_KEY_RSSI);
-        this.timeUsagePast7 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_7);
-        this.timeUsagePast30 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_30);
-        this.timeUsageToday = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_TODAY);
-        this.type = jsonObjectToString(jsonObject, JSON_KEY_TYPE);
-
-        if (this.hasLightEffect()) {
-            this.lightEffect = lightEffect.setData(jsonObject);
-        }
-    }
-
-    /***********************************
-     *
-     * CHECK FOR CHILD TYPES
-     *
-     ************************************/
-    public Boolean hasLightEffect() {
-        return this.jsonObject.has(JSON_KEY_LIGHTNING_EFFECT) || this.jsonObject.has(JSON_KEY_LIGHTNING_DYNAMIC_ENABLE);
-    }
-
-    /***********************************
-     *
-     * GET VALUES
-     *
-     ************************************/
-
-    public Integer getBrightness() {
-        return brightness;
-    }
-
-    public Integer getColorTemp() {
-        return colorTemp;
-    }
-
-    public String getFirmwareVersion() {
-        return fwVer;
-    }
-
-    public String getHardwareVersion() {
-        return hwVer;
-    }
-
-    public HSBType getHSB() {
-        DecimalType h = new DecimalType(hue);
-        PercentType s = new PercentType(saturation);
-        PercentType b = new PercentType(brightness);
-        return new HSBType(h, s, b);
-    }
-
-    public Integer getHue() {
-        return hue;
-    }
-
-    public TapoLightEffect getLightEffect() {
-        return lightEffect;
-    }
-
-    public String getIP() {
-        return ip;
-    }
-
-    public Boolean isOff() {
-        return !deviceOn;
-    }
-
-    public Boolean isOn() {
-        return deviceOn;
-    }
-
-    public Boolean isOverheated() {
-        return overheated;
-    }
-
-    public String getMAC() {
-        return formatMac(mac, MAC_DIVISION_CHAR);
-    }
-
-    public String getModel() {
-        return model.replace(" Series", "");
-    }
-
-    public String getNickname() {
-        return nickname;
-    }
-
-    public Number getOnTime() {
-        return onTime;
-    }
-
-    public String getRegion() {
-        return region;
-    }
-
-    public String getRepresentationProperty() {
-        return getMAC();
-    }
-
-    public Integer getSaturation() {
-        return saturation;
-    }
-
-    public String getSerial() {
-        return deviceId;
-    }
-
-    public Integer getSignalLevel() {
-        return signalLevel;
-    }
-
-    public Integer getRSSI() {
-        return rssi;
-    }
-
-    public Number getTimeUsagePast7() {
-        return timeUsagePast7;
-    }
-
-    public Number getTimeUsagePast30() {
-        return timeUsagePast30;
-    }
-
-    public Number getTimeUsagePastToday() {
-        return timeUsageToday;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    @Override
-    public String toString() {
-        return jsonObject.toString();
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoEnergyData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoEnergyData.java
deleted file mode 100644 (file)
index a18dcfe..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.JsonObject;
-
-/**
- * Tapo-Energy-Monitor Structure Class
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoEnergyData {
-    private Number currentPower = 0;
-    private Number todayEnergy = 0;
-    private Number monthEnergy = 0;
-    private Number todayRuntime = 0;
-    private Number monthRuntime = 0;
-    private Number[] past24h = new Number[24];
-    private Number[] past30d = new Number[30];
-    private Number[] past1y = new Number[12];
-
-    private JsonObject jsonObject = new JsonObject();
-
-    /**
-     * INIT
-     */
-    public TapoEnergyData() {
-        setData();
-    }
-
-    /**
-     * Init DeviceInfo with new Data;
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoEnergyData(JsonObject jso) {
-        setData(jso);
-    }
-
-    /**
-     * Set Data (new JsonObject)
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoEnergyData setData(JsonObject jso) {
-        /* create empty jsonObject to set efault values if has no energydata */
-        if (jso.has(JSON_KEY_ENERGY_POWER)) {
-            this.jsonObject = jso;
-        } else {
-            this.jsonObject = new JsonObject();
-        }
-        setData();
-        return this;
-    }
-
-    private void setData() {
-        this.currentPower = (float) jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_POWER) / 1000;
-
-        this.todayEnergy = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_ENERGY_TODAY);
-        this.monthEnergy = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_ENERGY_MONTH);
-        this.todayRuntime = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_RUNTIME_TODAY);
-        this.monthRuntime = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_RUNTIME_MONTH);
-        this.past24h = new Number[24];
-        this.past30d = new Number[30];
-        this.past1y = new Number[12];
-    }
-
-    /***********************************
-     *
-     * GET VALUES
-     *
-     ************************************/
-
-    public Number getCurrentPower() {
-        return currentPower;
-    }
-
-    public Number getTodayEnergy() {
-        return todayEnergy;
-    }
-
-    public Number getMonthEnergy() {
-        return monthEnergy;
-    }
-
-    public Number getYearEnergy() {
-        int sum = 0;
-        for (int i = 0; i < past1y.length; i++) {
-            sum += past1y[i].intValue();
-        }
-        return sum;
-    }
-
-    public Number getTodayRuntime() {
-        return todayRuntime;
-    }
-
-    public Number getMonthRuntime() {
-        return monthRuntime;
-    }
-
-    public Number[] getPast24hUsage() {
-        return past24h;
-    }
-
-    public Number[] getPast30dUsage() {
-        return past30d;
-    }
-
-    public Number[] getPast1yUsage() {
-        return past1y;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java
deleted file mode 100644 (file)
index 6ca0781..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.awt.Color;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.JsonObject;
-
-/**
- * Tapo-LightningEffect Structure Class
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoLightEffect {
-    private Boolean enable = false;
-    private String id = "";
-    private String name = "";
-    private Boolean custom = false;
-    private Integer brightness = 0;
-    private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
-    private Color[] displayColors = { Color.WHITE };
-
-    private JsonObject jsonObject = new JsonObject();
-
-    /**
-     * INIT
-     */
-    public TapoLightEffect() {
-    }
-
-    /**
-     * Init DeviceInfo with new Data;
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoLightEffect(JsonObject jso) {
-        setData(jso);
-    }
-
-    /**
-     * Set Data (new JsonObject)
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoLightEffect setData(JsonObject jso) {
-        /* create empty jsonObject to set efault values if has no lighning effect */
-        if (jso.has(JSON_KEY_LIGHTNING_EFFECT)) {
-            this.jsonObject = jso.getAsJsonObject(JSON_KEY_LIGHTNING_EFFECT);
-            this.enable = jsonObjectToBool(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ENABLE);
-            this.id = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ID, JSON_KEY_LIGHTNING_EFFECT_OFF);
-            this.name = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_NAME);
-            this.custom = jsonObjectToBool(jsonObject, JSON_KEY_LIGHTNING_EFFECT_CUSTOM);
-            this.brightness = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS);
-        } else if (jso.has(JSON_KEY_LIGHTNING_DYNAMIC_ENABLE)) {
-            this.jsonObject = jso;
-            this.enable = jsonObjectToBool(jsonObject, JSON_KEY_LIGHTNING_DYNAMIC_ENABLE);
-            this.id = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_DYNAMIC_ID, JSON_KEY_LIGHTNING_EFFECT_OFF);
-        } else {
-            setDefaults();
-        }
-        return this;
-    }
-
-    /**
-     * Set default values
-     */
-    private void setDefaults() {
-        this.jsonObject = new JsonObject();
-        this.enable = false;
-        this.id = JSON_KEY_LIGHTNING_EFFECT_OFF;
-        this.name = "";
-        this.custom = false;
-        this.brightness = 100;
-    }
-
-    /***********************************
-     *
-     * SET VALUES
-     *
-     ************************************/
-
-    public void setEnable(Boolean enable) {
-        this.enable = enable;
-    }
-
-    public void setName(String value) {
-        this.name = value;
-    }
-
-    public void setCustom(Boolean enable) {
-        this.custom = enable;
-    }
-
-    public void setBrightness(Integer value) {
-        this.brightness = value;
-    }
-
-    /***********************************
-     *
-     * GET VALUES
-     *
-     ************************************/
-
-    public Boolean getEnable() {
-        return enable;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public Boolean getCustom() {
-        return custom;
-    }
-
-    public Integer getBrightness() {
-        return brightness;
-    }
-
-    public Integer[] getColorTempRange() {
-        return colorTempRange;
-    }
-
-    public Color[] getDisplayColors() {
-        return displayColors;
-    }
-
-    @Override
-    public String toString() {
-        return jsonObject.toString();
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoSubRequest.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoSubRequest.java
deleted file mode 100644 (file)
index 96685e1..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * {@link TapoSubRequest} holds data sent to device in order to act on a child
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public record TapoSubRequest(String method, Object params) {
-    private record ChildRequest(String device_id, @SerializedName("requestData") TapoSubRequest requestData) {
-    }
-
-    private record SubMultiple(List<TapoSubRequest> requests) {
-
-        private SubMultiple(String method, TapoChild params) {
-            this(List.of(new TapoSubRequest(method, params)));
-        }
-    }
-
-    public TapoSubRequest(String deviceId, String method, TapoChild params) {
-        this(DEVICE_CMD_CONTROL_CHILD, new ChildRequest(deviceId,
-                new TapoSubRequest(DEVICE_CMD_MULTIPLE_REQ, new SubMultiple(method, params))));
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/bridgeconfig.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/bridgeconfig.xml
new file mode 100644 (file)
index 0000000..0fd08a8
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="bridge-type:tapo:bridge">
+               <parameter name="username" type="text" required="true">
+                       <context>email</context>
+                       <label>Username</label>
+                       <description>Tapo-Cloud Login User (e-Mail)</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <context>password</context>
+                       <label>Password</label>
+                       <description>Tapo-Cloud Login Password</description>
+               </parameter>
+               <parameter name="cloudDiscovery" type="boolean" required="false">
+                       <label>Cloud Discovery</label>
+                       <description>Use Cloud Discovery-Service to get all in Tapo-App registered devices. Includes DeviceName. IP-Address
+                               and Encryption has to set manually if device is not found via udpDiscovery</description>
+                       <default>false</default>
+                       <advanced>false</advanced>
+               </parameter>
+               <parameter name="udpDiscovery" type="boolean" required="false">
+                       <label>UDP Discovery</label>
+                       <description>Use UDP Discovery-Service to discover online devices in the local network. Includes Encryption and
+                               IP-Address</description>
+                       <default>false</default>
+                       <advanced>false</advanced>
+               </parameter>
+               <parameter name="onlyLocalOnlineDevices" type="boolean" required="false">
+                       <label>Discover only online devices</label>
+                       <description>Uses Cloud and UPD-Discovery to get more informations but will only discover online devices via UDP</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="broadcastAddress" type="text" required="false">
+                       <context>network-address</context>
+                       <label>Broadcast Address</label>
+                       <description>Broadcast-Address UDP-requests are sent to. Change it to the broadcast-address of your ip-range (e.g.
+                               '192.168.1.255') if you have problems with
+                               default (255.255.255.255)</description>
+                       <default>255.255.255.255</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="discoveryInterval" type="integer" min="1" max="10080" required="false">
+                       <label>Background Discovery Interval</label>
+                       <description>Interval background discovery in minutes (default 60)</description>
+                       <default>60</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml
deleted file mode 100644 (file)
index 09b5ef5..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<config-description:config-descriptions
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
-       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
-
-       <config-description uri="thing-type:tapo:device">
-               <parameter name="ipAddress" type="text" required="true">
-                       <context>network-address</context>
-                       <label>IP Address</label>
-               </parameter>
-               <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
-                       <label>Refresh Interval</label>
-                       <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
-                       <default>30</default>
-                       <advanced>true</advanced>
-               </parameter>
-       </config-description>
-
-       <config-description uri="bridge-type:tapo:bridge">
-               <parameter name="username" type="text" required="true">
-                       <context>email</context>
-                       <label>Username</label>
-                       <description>Tapo-Cloud Login User (e-Mail)</description>
-               </parameter>
-               <parameter name="password" type="text" required="true">
-                       <context>password</context>
-                       <label>Password</label>
-                       <description>Tapo-Cloud Login Password</description>
-               </parameter>
-               <parameter name="cloudDiscovery" type="boolean" required="false">
-                       <label>Cloud Discovery</label>
-                       <description>Use Cloud Discovery-Service</description>
-                       <default>false</default>
-                       <advanced>false</advanced>
-               </parameter>
-               <parameter name="discoveryInterval" type="integer" min="1" max="10080" required="false">
-                       <label>Background Discovery Interval</label>
-                       <description>Interval background discovery in minutes (default 60)</description>
-                       <default>60</default>
-                       <advanced>true</advanced>
-               </parameter>
-       </config-description>
-</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/deviceconfig.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/deviceconfig.xml
new file mode 100644 (file)
index 0000000..97a5ed2
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:tapo:device">
+               <parameter name="ipAddress" type="text" required="true">
+                       <context>network-address</context>
+                       <label>IP Address</label>
+               </parameter>
+               <parameter name="httpPort" type="integer" required="true">
+                       <label>Port</label>
+                       <description>HTTP-Communication Port (default 80)</description>
+                       <default>80</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="protocol" type="text" required="true">
+                       <label>Protocol</label>
+                       <description>Communication Protocol</description>
+                       <options>
+                               <option value="">Direct HTTP</option>
+                               <option value="AES">Secured HTTP Protocol</option>
+                               <option value="KLAP">Secured KLAP HTTP Protocol</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <default>AES</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
+                       <label>Refresh Interval</label>
+                       <description>Refresh interval for refreshing the data in seconds (0=disabled). Set it below 10 seconds may cause
+                               communication issues (not recommed).</description>
+                       <default>30</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/hubconfig.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/hubconfig.xml
new file mode 100644 (file)
index 0000000..48fd348
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="bridge-type:tapo:hub">
+               <parameter name="ipAddress" type="text" required="true">
+                       <context>network-address</context>
+                       <label>IP Address</label>
+               </parameter>
+               <parameter name="httpPort" type="integer" required="true">
+                       <label>Port</label>
+                       <description>HTTP-Communication Port (default 80)</description>
+                       <default>80</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="protocol" type="text" required="true">
+                       <label>Protocol</label>
+                       <description>Communication Protocol</description>
+                       <options>
+                               <option value="">Direct HTTP</option>
+                               <option value="AES">Secured HTTP Protocol</option>
+                               <option value="KLAP">Secured KLAP HTTP Protocol</option>
+                       </options>
+                       <limitToOptions>true</limitToOptions>
+                       <default>AES</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
+                       <label>Refresh Interval</label>
+                       <description>Refresh interval for refreshing hub informations in seconds (0=disabled). Set it below 10 seconds may
+                               cause communication issues (not recommed).</description>
+                       <default>10</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="backgroundDiscovery" type="boolean" required="false">
+                       <label>Background Discovery</label>
+                       <description>If background discovery is enabled, devices will be discovered after every polling request</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
index ed585295995b7e60e5d0d1142ae36f466bc80aa2..2feb90018f7b614cba67a1ef0331ae9505b16b03 100644 (file)
@@ -5,6 +5,8 @@ addon.tapocontrol.description = Control your TAPO-SmartHome Devices
 
 # thing types
 
+thing-type.tapocontrol.H100.label = Tapo Hub H100
+thing-type.tapocontrol.H100.description = Tapo SmartHub H100
 thing-type.tapocontrol.L510.label = L510 Series White-Bulb
 thing-type.tapocontrol.L510.description = Tapo Smart dimmable White-Light-Bulb
 thing-type.tapocontrol.L530.label = L530 Series Color-Bulb
@@ -29,27 +31,62 @@ thing-type.tapocontrol.P115.label = P115 SmartPlug
 thing-type.tapocontrol.P115.description = Tapo Smart Monitoring Wifi Plug
 thing-type.tapocontrol.P300.label = P300 Power Strip
 thing-type.tapocontrol.P300.description = Tapo Smart Wi-Fi Power Strip
+thing-type.tapocontrol.T110.label = T110 Smart Contact Sensor
+thing-type.tapocontrol.T110.description = Tapo Smart Window/Door Sensor
+thing-type.tapocontrol.T310.label = T310 Temperature Sensor
+thing-type.tapocontrol.T310.description = Tapo Smart Temperature and Humidity Sensor
+thing-type.tapocontrol.T315.label = T315 Temperature Monitor
+thing-type.tapocontrol.T315.description = Tapo Smart Temperature and Humidity Monitor
 thing-type.tapocontrol.bridge.label = Cloud-Login
 thing-type.tapocontrol.bridge.description = Cloud Connector. Acts as device-bridge
 
 # thing types config
 
+bridge-type.config.tapo.bridge.broadcastAddress.label = Broadcast Address
+bridge-type.config.tapo.bridge.broadcastAddress.description = Broadcast-Address UDP-requests are sent to. Change it to the broadcast-address of your ip-range (e.g. '192.168.1.255') if you have problems with default (255.255.255.255)
 bridge-type.config.tapo.bridge.cloudDiscovery.label = Cloud Discovery
-bridge-type.config.tapo.bridge.cloudDiscovery.description = Use Cloud Discovery-Service
+bridge-type.config.tapo.bridge.cloudDiscovery.description = Use Cloud Discovery-Service to get all in Tapo-App registered devices. Includes DeviceName. IP-Address and Encryption has to set manually if device is not found via udpDiscovery
 bridge-type.config.tapo.bridge.discoveryInterval.label = Background Discovery Interval
 bridge-type.config.tapo.bridge.discoveryInterval.description = Interval background discovery in minutes (default 60)
+bridge-type.config.tapo.bridge.onlyLocalOnlineDevices.label = Discover only online devices
+bridge-type.config.tapo.bridge.onlyLocalOnlineDevices.description = Uses Cloud and UPD-Discovery to get more informations but will only discover online devices via UDP
 bridge-type.config.tapo.bridge.password.label = Password
 bridge-type.config.tapo.bridge.password.description = Tapo-Cloud Login Password
+bridge-type.config.tapo.bridge.udpDiscovery.label = UDP Discovery
+bridge-type.config.tapo.bridge.udpDiscovery.description = Use UDP Discovery-Service to discover online devices in the local network. Includes Encryption and IP-Address
 bridge-type.config.tapo.bridge.username.label = Username
 bridge-type.config.tapo.bridge.username.description = Tapo-Cloud Login User (e-Mail)
+bridge-type.config.tapo.hub.backgroundDiscovery.label = Background Discovery
+bridge-type.config.tapo.hub.backgroundDiscovery.description = If background discovery is enabled, devices will be discovered after every polling request
+bridge-type.config.tapo.hub.httpPort.label = Port
+bridge-type.config.tapo.hub.httpPort.description = HTTP-Communication Port (default 80)
+bridge-type.config.tapo.hub.ipAddress.label = IP Address
+bridge-type.config.tapo.hub.pollingInterval.label = Refresh Interval
+bridge-type.config.tapo.hub.pollingInterval.description = Refresh interval for refreshing hub informations in seconds (0=disabled). Set it below 10 seconds may cause communication issues (not recommed).
+bridge-type.config.tapo.hub.protocol.label = Protocol
+bridge-type.config.tapo.hub.protocol.description = Communication Protocol
+bridge-type.config.tapo.hub.protocol.option. = Direct HTTP
+bridge-type.config.tapo.hub.protocol.option.AES = Secured HTTP Protocol
+bridge-type.config.tapo.hub.protocol.option.KLAP = Secured KLAP HTTP Protocol
+thing-type.config.tapo.device.httpPort.label = Port
+thing-type.config.tapo.device.httpPort.description = HTTP-Communication Port (default 80)
 thing-type.config.tapo.device.ipAddress.label = IP Address
 thing-type.config.tapo.device.pollingInterval.label = Refresh Interval
-thing-type.config.tapo.device.pollingInterval.description = Refresh interval for refreshing the data in seconds. (0=disabled)
+thing-type.config.tapo.device.pollingInterval.description = Refresh interval for refreshing the data in seconds (0=disabled). Set it below 10 seconds may cause communication issues (not recommed).
+thing-type.config.tapo.device.protocol.label = Protocol
+thing-type.config.tapo.device.protocol.description = Communication Protocol
+thing-type.config.tapo.device.protocol.option. = Direct HTTP
+thing-type.config.tapo.device.protocol.option.AES = Secured HTTP Protocol
+thing-type.config.tapo.device.protocol.option.KLAP = Secured KLAP HTTP Protocol
 
 # channel group types
 
+channel-group-type.tapocontrol.childDeviceState.label = Device State
+channel-group-type.tapocontrol.childDeviceState.description = Information about the device
 channel-group-type.tapocontrol.colorBulb.label = Color Light Bulb
 channel-group-type.tapocontrol.colorBulb.description = Tapo Multicolor Smart Light Bulb
+channel-group-type.tapocontrol.contactSensor.label = Contact Sensor
+channel-group-type.tapocontrol.contactSensor.description = Door/Window Contact Sensor Channels
 channel-group-type.tapocontrol.deviceState.label = Device State
 channel-group-type.tapocontrol.deviceState.description = Information about the device
 channel-group-type.tapocontrol.deviceStateS.label = Device State
@@ -60,10 +97,12 @@ channel-group-type.tapocontrol.lightBulb.label = Light Bulb
 channel-group-type.tapocontrol.lightBulb.description = Tapo Smart Light Bulb
 channel-group-type.tapocontrol.lightEffectL530.label = Lightning Effect
 channel-group-type.tapocontrol.lightEffectL530.description = Tapo Lightning Effects
+channel-group-type.tapocontrol.lightEffectL900.label = Lightning Effect
+channel-group-type.tapocontrol.lightEffectL900.description = Tapo Lightning Effects
 channel-group-type.tapocontrol.lightEffectL920.label = Lightning Effect
 channel-group-type.tapocontrol.lightEffectL920.description = Tapo Lightning Effects
-channel-group-type.tapocontrol.lightEffectL920.label = Lightning Effect
-channel-group-type.tapocontrol.lightEffectL920.description = Tapo Lightning Effects
+channel-group-type.tapocontrol.lightEffectL930.label = Lightning Effect
+channel-group-type.tapocontrol.lightEffectL930.description = Tapo Lightning Effects
 channel-group-type.tapocontrol.lightStrip.label = Color Light Strip
 channel-group-type.tapocontrol.lightStrip.description = Tapo Multicolor Smart Light Strip
 channel-group-type.tapocontrol.powerStrip.label = SmartPlug
@@ -74,30 +113,77 @@ channel-group-type.tapocontrol.powerStrip.channel.output2.label = Output Switch
 channel-group-type.tapocontrol.powerStrip.channel.output2.description = Switches the power state on/off of the second socket
 channel-group-type.tapocontrol.powerStrip.channel.output3.label = Output Switch 3
 channel-group-type.tapocontrol.powerStrip.channel.output3.description = Switches the power state on/off of the third socket
+channel-group-type.tapocontrol.smartHubAlarms.label = Smart Hub Alarms
+channel-group-type.tapocontrol.smartHubAlarms.description = Tapo Smart Hub Alarms
 channel-group-type.tapocontrol.smartPlug.label = SmartPlug
 channel-group-type.tapocontrol.smartPlug.description = Tapo Smart Plug Power Outlet
+channel-group-type.tapocontrol.temperatureSensor.label = Wheater Sensor
+channel-group-type.tapocontrol.temperatureSensor.description = Temperature and Humidity Sensor Channels
 
 # channel types
 
 channel-type.tapocontrol.actualPowerChannel.label = Power
 channel-type.tapocontrol.actualPowerChannel.description = Actual power usage
+channel-type.tapocontrol.alarmIsActiveChannel.label = Active Alarm
+channel-type.tapocontrol.alarmIsActiveChannel.description = Device has an active alarm
+channel-type.tapocontrol.alarmSourceChannel.label = Alarm Source
+channel-type.tapocontrol.alarmSourceChannel.description = Source of active alarm
+channel-type.tapocontrol.batteryLowChannel.label = Battery Low
+channel-type.tapocontrol.batteryLowChannel.description = Battery of device is low
+channel-type.tapocontrol.batteryLowEvent.label = Battery Low
+channel-type.tapocontrol.batteryLowEvent.description = Battery state has changed to low. Replace Battery
+channel-type.tapocontrol.colorBulbMode.label = Mode
+channel-type.tapocontrol.colorBulbMode.description = Working mode of device
+channel-type.tapocontrol.colorBulbMode.command.option.WHITE_LIGHT = White
+channel-type.tapocontrol.colorBulbMode.command.option.COLOR_LIGHT = Color
+channel-type.tapocontrol.colorBulbMode.command.option.LIGHT_FX = Effects
 channel-type.tapocontrol.colorChannel.label = Color
 channel-type.tapocontrol.colorChannel.description = Color
 channel-type.tapocontrol.colorTemperature.label = Color Temperature
-channel-type.tapocontrol.colorTemperature.description = This channel supports adjusting the color temperature from 2700K to 6500K.
+channel-type.tapocontrol.colorTemperature.description = This channel supports adjusting the color temperature from 2200K to 6500K.
+channel-type.tapocontrol.contactCloseEvent.label = Contact Closed
+channel-type.tapocontrol.contactCloseEvent.description = Event is fired if contact changes from open to closed
+channel-type.tapocontrol.contactOpenEvent.label = Contact Opened
+channel-type.tapocontrol.contactOpenEvent.description = Event is fired if contact changes from closed to open
 channel-type.tapocontrol.dimmerChannel.label = Brightness
 channel-type.tapocontrol.dimmerChannel.description = Brightness
 channel-type.tapocontrol.fade.label = Fade Light
 channel-type.tapocontrol.fade.description = Make the light darker or lighter slowly
+channel-type.tapocontrol.humidityChannel.label = Humidity
+channel-type.tapocontrol.humidityChannel.description = Current relative humidity
+channel-type.tapocontrol.isOnlineChannel.label = Is Online
+channel-type.tapocontrol.isOnlineChannel.description = Device is Online
+channel-type.tapocontrol.isOpenChannel.label = Is Open
+channel-type.tapocontrol.isOpenChannel.description = Contact (Window/Door) is Open
 channel-type.tapocontrol.l530fxList.label = Light Effect Theme
 channel-type.tapocontrol.l530fxList.description = Name of active lightning effect
-channel-type.tapocontrol.l530fxList.state.option.off = None (No FX)
-channel-type.tapocontrol.l530fxList.state.option.custom = Custom
-channel-type.tapocontrol.l530fxList.state.option.L1 = Party
-channel-type.tapocontrol.l530fxList.state.option.L2 = Relax
+channel-type.tapocontrol.l530fxList.command.option.off = None (No FX)
+channel-type.tapocontrol.l530fxList.command.option.custom = Custom
+channel-type.tapocontrol.l530fxList.command.option.L1 = Party
+channel-type.tapocontrol.l530fxList.command.option.L2 = Relax
+channel-type.tapocontrol.l900fxList.label = Light Effect Theme
+channel-type.tapocontrol.l900fxList.description = Name of lightning effect
+channel-type.tapocontrol.l900fxList.state.option.off = None (No FX)
+channel-type.tapocontrol.l900fxList.state.option.aurora = Aurora
+channel-type.tapocontrol.l900fxList.state.option.bubbling_calderon = Bubbling Calderon
+channel-type.tapocontrol.l900fxList.state.option.christmas = Christmas
+channel-type.tapocontrol.l900fxList.state.option.christmas_light = Christmas Light
+channel-type.tapocontrol.l900fxList.state.option.candy_cane = Candy
+channel-type.tapocontrol.l900fxList.state.option.flicker = Flicker
+channel-type.tapocontrol.l900fxList.state.option.hanukkah = Hanukkah
+channel-type.tapocontrol.l900fxList.state.option.haunted_mansion = Haunted Mansion
+channel-type.tapocontrol.l900fxList.state.option.icicle = Icicle
+channel-type.tapocontrol.l900fxList.state.option.lightning = Lightning
+channel-type.tapocontrol.l900fxList.state.option.ocean = Ocean
+channel-type.tapocontrol.l900fxList.state.option.rainbow = Rainbow
+channel-type.tapocontrol.l900fxList.state.option.raindrop = Raindrop
+channel-type.tapocontrol.l900fxList.state.option.spring = Spring
+channel-type.tapocontrol.l900fxList.state.option.sunrise = Sunrise
+channel-type.tapocontrol.l900fxList.state.option.sunset = Sunset
+channel-type.tapocontrol.l900fxList.state.option.valentines = Valentines
 channel-type.tapocontrol.l920fxList.label = Light Effect Theme
 channel-type.tapocontrol.l920fxList.description = Name of lightning effect
-channel-type.tapocontrol.l920fxList.state.option. = None (No FX)
+channel-type.tapocontrol.l920fxList.state.option.off = None (No FX)
 channel-type.tapocontrol.l920fxList.state.option.aurora = Aurora
 channel-type.tapocontrol.l920fxList.state.option.bubbling_calderon = Bubbling Calderon
 channel-type.tapocontrol.l920fxList.state.option.christmas = Christmas
@@ -117,7 +203,7 @@ channel-type.tapocontrol.l920fxList.state.option.sunset = Sunset
 channel-type.tapocontrol.l920fxList.state.option.valentines = Valentines
 channel-type.tapocontrol.l930fxList.label = Light Effect Theme
 channel-type.tapocontrol.l930fxList.description = Name of lightning effect
-channel-type.tapocontrol.l930fxList.state.option. = None (No FX)
+channel-type.tapocontrol.l930fxList.state.option.off = None (No FX)
 channel-type.tapocontrol.l930fxList.state.option.aurora = Aurora
 channel-type.tapocontrol.l930fxList.state.option.bubbling_calderon = Bubbling Calderon
 channel-type.tapocontrol.l930fxList.state.option.christmas = Christmas
@@ -139,17 +225,47 @@ channel-type.tapocontrol.led.label = Switch Led
 channel-type.tapocontrol.led.description = Switch the Smart Home device led on or off.
 channel-type.tapocontrol.lightOn.label = Light On
 channel-type.tapocontrol.lightOn.description = Switches the light on/off
+channel-type.tapocontrol.monthEnergyUsageChannel.label = Month Usage
+channel-type.tapocontrol.monthEnergyUsageChannel.description = Energy usage last month
+channel-type.tapocontrol.monthRuntimeChannel.label = Month Runtime
+channel-type.tapocontrol.monthRuntimeChannel.description = Runtime last month (On-Time)
 channel-type.tapocontrol.ontime.label = On-Time
 channel-type.tapocontrol.ontime.description = Number of seconds since the device was powered on
 channel-type.tapocontrol.outputChannel.label = Output Switch
 channel-type.tapocontrol.outputChannel.description = Switches the power state on/off
 channel-type.tapocontrol.overheated.label = Device Overheated
 channel-type.tapocontrol.overheated.description = ON if device is overheated
+channel-type.tapocontrol.temperatureChannel.label = Temperature
+channel-type.tapocontrol.temperatureChannel.description = Temperature as measured by the sensor
 channel-type.tapocontrol.todayEnergyUsageChannel.label = Today Usage
 channel-type.tapocontrol.todayEnergyUsageChannel.description = Today energy usage
 channel-type.tapocontrol.todayRuntimeChannel.label = Today Runtime
 channel-type.tapocontrol.todayRuntimeChannel.description = Today runtime (On-Time)
 
+# channel types
+
+channel-type.tapocontrol.l530fxList.state.option.off = None (No FX)
+channel-type.tapocontrol.l530fxList.state.option.custom = Custom
+channel-type.tapocontrol.l530fxList.state.option.L1 = Party
+channel-type.tapocontrol.l530fxList.state.option.L2 = Relax
+
+# thing types config
+
+bridge-type.config.tapo.hub.protocol.option.passthrough = Direct HTTP
+bridge-type.config.tapo.hub.protocol.option.securePassthrough = Secured HTTP Protocol
+bridge-type.config.tapo.hub.protocol.option.klap = Secured KLAP HTTP Protocol
+
+# thing types config
+
+thing-type.config.tapo.device.protocol.option.passthrough = Direct HTTP
+thing-type.config.tapo.device.protocol.option.securePassthrough = Secured HTTP Protocol
+thing-type.config.tapo.device.protocol.option.klap = Secured KLAP HTTP Protocol
+
+# channel types
+
+channel-type.tapocontrol.l920fxList.state.option. = None (No FX)
+channel-type.tapocontrol.l930fxList.state.option. = None (No FX)
+
 # channel group types
 
 channel-group-type.tapocontrol.lightEffect.label = Lightning Effect
@@ -166,6 +282,7 @@ channel-type.tapocontrol.effectOn.description = Switches the lightning effect on
 
 # error messages
 
+error-api-unknown-com-error = recieived unknown com error(9999)
 error-api-account = received account error (-2101)
 error-api-aes-decode-fail = aes decode failed (-1005)
 error-api-antitheft-conflict = device antitheft conflict (-2002)
@@ -190,6 +307,7 @@ error-api-login = invalid request or credentials (-1501)
 error-api-multi-request-failed = multi request failed (1200)
 error-api-null-transport = null transport error (1000)
 error-api-params = received invalid parameter (-1008)
+error-api-protocol = encryption protocol error (1003)
 error-api-quick-setup = quick setup error (-1201)
 error-api-request-len-error = request length error (-1006)
 error-api-request = invalid request or command (1002)
@@ -211,6 +329,7 @@ error-api-wireless-unsupported = wireless unsuported error (-1702)
 error-binding-connect-timeout = connection timeout - device not reachable (9010)
 error-binding-cookie = cookie error (9002)
 error-binding-credentials = invalid request or credentials (9003)
+error-binding-login = login failed (9004)
 error-binding-device-offline = device offline (9009)
 error-binding-http-response = invalid http-response (9001)
 error-cloud-api-rate = api rate limit exceeded (-20004)
@@ -222,4 +341,9 @@ error-cloud-token-expired = token expired (-20651)
 error-config-credentials = credentials not set (bridge) (10002)
 error-config-ip = ip-address not valid (10001)
 error-config-no-bridge = no bridge configured (10003)
+error-config-protocol = unknown protocol (10004)
 error-unknown = unknown api error ({0})
+error-data-encrypting = encrypting failed (9500)
+error-data-decrypting = decrypting failed (9501)
+error-data-format = unexpected data format (9505)
+error-data-transormation = data transformation failed (9506)
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/H100.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/H100.xml
new file mode 100644 (file)
index 0000000..6f5d497
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="H100">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>Tapo Hub H100</label>
+               <description>Tapo SmartHub H100</description>
+
+               <channel-groups>
+                       <channel-group id="alarm" typeId="smartHubAlarms"/>
+                       <channel-group id="device" typeId="deviceStateS"/>
+               </channel-groups>
+
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="bridge-type:tapo:hub"/>
+       </bridge-type>
+</thing:thing-descriptions>
index 185d98cc95d3c6a7560783b7abbc00613d4fa87e..73074332f6baac2d09a6a13ad5d4642b2f1a2308 100644 (file)
                <item-type>String</item-type>
                <label>Light Effect Theme</label>
                <description>Name of active lightning effect</description>
-               <state readOnly="false">
+               <state readOnly="false"/>
+               <command>
                        <options>
                                <option value="off">None (No FX)</option>
                                <option value="custom">Custom</option>
                                <option value="L1">Party</option>
                                <option value="L2">Relax</option>
                        </options>
-               </state>
+               </command>
        </channel-type>
 </thing:thing-descriptions>
index d6948c8f79426799eaac39e6e6e92f24a9432afc..1c49f5f37852d95e269519980195b82d1465782e 100644 (file)
                <description>Tapo Smart LED-Lightstrip</description>
                <channel-groups>
                        <channel-group id="actuator" typeId="lightStrip"/>
+                       <channel-group id="effects" typeId="lightEffectL900"/>
                        <channel-group id="device" typeId="deviceState"/>
                </channel-groups>
                <representation-property>macAddress</representation-property>
 
                <config-description-ref uri="thing-type:tapo:device"/>
        </thing-type>
+
+       <!-- Lightning Effect -->
+       <channel-group-type id="lightEffectL900">
+               <label>Lightning Effect</label>
+               <description>Tapo Lightning Effects</description>
+               <channels>
+                       <channel id="fxName" typeId="l900fxList"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- effect name -->
+       <channel-type id="l900fxList">
+               <item-type>String</item-type>
+               <label>Light Effect Theme</label>
+               <description>Name of lightning effect</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="off">None (No FX)</option>
+                               <option value="aurora">Aurora</option>
+                               <option value="bubbling_calderon">Bubbling Calderon</option>
+                               <option value="christmas">Christmas</option>
+                               <option value="christmas_light">Christmas Light</option>
+                               <option value="candy_cane">Candy</option>
+                               <option value="flicker">Flicker</option>
+                               <option value="hanukkah">Hanukkah</option>
+                               <option value="haunted_mansion">Haunted Mansion</option>
+                               <option value="icicle">Icicle</option>
+                               <option value="lightning">Lightning</option>
+                               <option value="ocean">Ocean</option>
+                               <option value="rainbow">Rainbow</option>
+                               <option value="raindrop">Raindrop</option>
+                               <option value="spring">Spring</option>
+                               <option value="sunrise">Sunrise</option>
+                               <option value="sunset">Sunset</option>
+                               <option value="valentines">Valentines</option>
+                       </options>
+               </state>
+       </channel-type>
 </thing:thing-descriptions>
index 0588cae99cd239f6d87b986ab98412eeb524a88d..12b3e65f7250ba9ab548628e7075f00e3d479b5e 100644 (file)
@@ -14,6 +14,7 @@
                <description>Tapo Smart Multicolor LED-Lightstrip</description>
                <channel-groups>
                        <channel-group id="actuator" typeId="lightStrip"/>
+                       <channel-group id="effects" typeId="lightEffectL920"/>
                        <channel-group id="device" typeId="deviceState"/>
                </channel-groups>
                <representation-property>macAddress</representation-property>
                <label>Lightning Effect</label>
                <description>Tapo Lightning Effects</description>
                <channels>
-                       <channel id="fxBrightness" typeId="dimmerChannel"/>
                        <channel id="fxName" typeId="l920fxList"/>
-                       <channel id="fxColor1" typeId="colorChannel"/>
-                       <channel id="fxColor2" typeId="colorChannel"/>
-                       <channel id="fxColor3" typeId="colorChannel"/>
-                       <channel id="fxColor4" typeId="colorChannel"/>
                </channels>
        </channel-group-type>
 
@@ -42,7 +38,7 @@
                <description>Name of lightning effect</description>
                <state readOnly="false">
                        <options>
-                               <option value="">None (No FX)</option>
+                               <option value="off">None (No FX)</option>
                                <option value="aurora">Aurora</option>
                                <option value="bubbling_calderon">Bubbling Calderon</option>
                                <option value="christmas">Christmas</option>
index 359b6e63b8e11bcf97c88dfe90967ca57e7da46c..ff79e8feb309b92c938a75f4584620f0aeded174 100644 (file)
@@ -14,6 +14,7 @@
                <description>Tapo Smart Multicolor LED-Lightstrip with ZoneControl</description>
                <channel-groups>
                        <channel-group id="actuator" typeId="lightStrip"/>
+                       <channel-group id="effects" typeId="lightEffectL930"/>
                        <channel-group id="device" typeId="deviceState"/>
                </channel-groups>
                <representation-property>macAddress</representation-property>
        </thing-type>
 
        <!-- Lightning Effect -->
-       <channel-group-type id="lightEffectL920">
+       <channel-group-type id="lightEffectL930">
                <label>Lightning Effect</label>
                <description>Tapo Lightning Effects</description>
                <channels>
-                       <channel id="fxBrightness" typeId="dimmerChannel"/>
                        <channel id="fxName" typeId="l930fxList"/>
-                       <channel id="fxColor1" typeId="colorChannel"/>
-                       <channel id="fxColor2" typeId="colorChannel"/>
-                       <channel id="fxColor3" typeId="colorChannel"/>
-                       <channel id="fxColor4" typeId="colorChannel"/>
                </channels>
        </channel-group-type>
 
@@ -42,7 +38,7 @@
                <description>Name of lightning effect</description>
                <state readOnly="false">
                        <options>
-                               <option value="">None (No FX)</option>
+                               <option value="off">None (No FX)</option>
                                <option value="aurora">Aurora</option>
                                <option value="bubbling_calderon">Bubbling Calderon</option>
                                <option value="christmas">Christmas</option>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T110.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T110.xml
new file mode 100644 (file)
index 0000000..c5f7d22
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- T110 THING-TYPE (SMART CONTACT SENSOR) -->
+       <thing-type id="T110">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="H100"/>
+               </supported-bridge-type-refs>
+
+               <label>T110 Smart Contact Sensor</label>
+               <description>Tapo Smart Window/Door Sensor</description>
+               <channel-groups>
+                       <channel-group id="sensor" typeId="contactSensor"/>
+                       <channel-group id="device" typeId="childDeviceState"/>
+               </channel-groups>
+               <representation-property>serialNumber</representation-property>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T310.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T310.xml
new file mode 100644 (file)
index 0000000..9a5a511
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- T310 THING-TYPE (SMART CONTACT SENSOR) -->
+       <thing-type id="T310">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="H100"/>
+               </supported-bridge-type-refs>
+
+               <label>T310 Temperature Sensor</label>
+               <description>Tapo Smart Temperature and Humidity Sensor</description>
+
+               <channel-groups>
+                       <channel-group id="sensor" typeId="temperatureSensor"/>
+                       <channel-group id="device" typeId="childDeviceState"/>
+               </channel-groups>
+               <representation-property>serialNumber</representation-property>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T315.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T315.xml
new file mode 100644 (file)
index 0000000..76112dd
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- T315 THING-TYPE (SMART CONTACT SENSOR) -->
+       <thing-type id="T315">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="H100"/>
+               </supported-bridge-type-refs>
+
+               <label>T315 Temperature Monitor</label>
+               <description>Tapo Smart Temperature and Humidity Monitor</description>
+
+               <channel-groups>
+                       <channel-group id="sensor" typeId="temperatureSensor"/>
+                       <channel-group id="device" typeId="childDeviceState"/>
+               </channel-groups>
+               <representation-property>serialNumber</representation-property>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channelgroups.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channelgroups.xml
new file mode 100644 (file)
index 0000000..624be0b
--- /dev/null
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- ############################### CHANNEL-GROUPS ############################### -->
+
+       <!-- CHANNEL GROUP TYPES -->
+       <!-- Device-Status Channel Type -->
+       <channel-group-type id="deviceState">
+               <label>Device State</label>
+               <description>Information about the device</description>
+               <channels>
+                       <channel id="wifiSignal" typeId="system.signal-strength"/>
+                       <channel id="onTime" typeId="ontime"/>
+                       <channel id="overheated" typeId="overheated"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- Device-Status Channel Type (Small) -->
+       <channel-group-type id="deviceStateS">
+               <label>Device State</label>
+               <description>Information about the device</description>
+               <channels>
+                       <channel id="wifiSignal" typeId="system.signal-strength"/>
+                       <channel id="overheated" typeId="overheated"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- Child-Device-Status Channel Type -->
+       <channel-group-type id="childDeviceState">
+               <label>Device State</label>
+               <description>Information about the device</description>
+               <channels>
+                       <channel id="signalStrength" typeId="system.signal-strength"/>
+                       <channel id="batteryLow" typeId="batteryLowChannel"/>
+                       <channel id="batteryIsLow" typeId="batteryLowEvent"></channel>
+               </channels>
+       </channel-group-type>
+
+       <!-- Actor Channel Type -->
+       <channel-group-type id="smartPlug">
+               <label>SmartPlug</label>
+               <description>Tapo Smart Plug Power Outlet</description>
+               <channels>
+                       <channel id="output" typeId="outputChannel"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="powerStrip">
+               <label>SmartPlug</label>
+               <description>Tapo Smart Plug Power Outlet</description>
+               <channels>
+                       <channel id="output1" typeId="outputChannel">
+                               <label>Output Switch 1</label>
+                               <description>Switches the power state on/off of the first socket</description>
+                       </channel>
+                       <channel id="output2" typeId="outputChannel">
+                               <label>Output Switch 2</label>
+                               <description>Switches the power state on/off of the second socket</description>
+                       </channel>
+                       <channel id="output3" typeId="outputChannel">
+                               <label>Output Switch 3</label>
+                               <description>Switches the power state on/off of the third socket</description>
+                       </channel>
+               </channels>
+       </channel-group-type>
+
+       <!-- Light-Bulb Channel Type -->
+       <channel-group-type id="lightBulb">
+               <label>Light Bulb</label>
+               <description>Tapo Smart Light Bulb</description>
+               <channels>
+                       <channel id="output" typeId="lightOn"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- Color Channel Type -->
+       <channel-group-type id="colorBulb">
+               <label>Color Light Bulb</label>
+               <description>Tapo Multicolor Smart Light Bulb</description>
+               <channels>
+                       <channel id="output" typeId="lightOn"/>
+                       <channel id="mode" typeId="colorBulbMode"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+                       <channel id="color" typeId="colorChannel"/>
+                       <channel id="colorTemperature" typeId="colorTemperature"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- LightStrip -->
+       <channel-group-type id="lightStrip">
+               <label>Color Light Strip</label>
+               <description>Tapo Multicolor Smart Light Strip</description>
+               <channels>
+                       <channel id="output" typeId="lightOn"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+                       <channel id="color" typeId="colorChannel"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- Energy Monitor -->
+       <channel-group-type id="energyMonitor">
+               <label>Energy Usage</label>
+               <description>Energy and Power usage</description>
+               <channels>
+                       <channel id="actualPower" typeId="actualPowerChannel"></channel>
+                       <channel id="todayEnergyUsage" typeId="todayEnergyUsageChannel"></channel>
+                       <channel id="todayRuntime" typeId="todayRuntimeChannel"></channel>
+                       <channel id="monthEnergyUsage" typeId="monthEnergyUsageChannel"></channel>
+                       <channel id="monthRuntime" typeId="monthRuntimeChannel"></channel>
+               </channels>
+       </channel-group-type>
+
+       <!-- Smart Hub -->
+       <channel-group-type id="smartHubAlarms">
+               <label>Smart Hub Alarms</label>
+               <description>Tapo Smart Hub Alarms</description>
+               <channels>
+                       <channel id="alarmActive" typeId="alarmIsActiveChannel"></channel>
+                       <channel id="alarmSource" typeId="alarmSourceChannel"></channel>
+               </channels>
+       </channel-group-type>
+
+       <!-- Contact Sensor -->
+       <channel-group-type id="contactSensor">
+               <label>Contact Sensor</label>
+               <description>Door/Window Contact Sensor Channels</description>
+               <channels>
+                       <channel id="isOpen" typeId="isOpenChannel"></channel>
+                       <channel id="contactOpened" typeId="contactOpenEvent"></channel>
+                       <channel id="contactClosed" typeId="contactCloseEvent"></channel>
+               </channels>
+       </channel-group-type>
+
+       <!-- Temperature Sensor -->
+       <channel-group-type id="temperatureSensor">
+               <label>Wheater Sensor</label>
+               <description>Temperature and Humidity Sensor Channels</description>
+               <channels>
+                       <channel id="currentTemp" typeId="temperatureChannel"></channel>
+                       <channel id="currentHumidity" typeId="humidityChannel"></channel>
+               </channels>
+       </channel-group-type>
+
+</thing:thing-descriptions>
index a2b80ec289f41f3c3377b600dd351f3b1a6e2df4..eb99925c062282751920249931e52dfcfe0c35c5 100644 (file)
@@ -4,104 +4,6 @@
        xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
        xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
 
-       <!-- ############################### CHANNEL-GROUPS ############################### -->
-
-       <!-- CHANNEL GROUP TYPES -->
-       <!--Device-Status Channel Type -->
-       <channel-group-type id="deviceState">
-               <label>Device State</label>
-               <description>Information about the device</description>
-               <channels>
-                       <channel id="wifiSignal" typeId="system.signal-strength"/>
-                       <channel id="onTime" typeId="ontime"/>
-                       <channel id="overheated" typeId="overheated"/>
-               </channels>
-       </channel-group-type>
-
-       <!--Device-Status Channel Type (Small) -->
-       <channel-group-type id="deviceStateS">
-               <label>Device State</label>
-               <description>Information about the device</description>
-               <channels>
-                       <channel id="wifiSignal" typeId="system.signal-strength"/>
-                       <channel id="overheated" typeId="overheated"/>
-               </channels>
-       </channel-group-type>
-
-       <!--Actor Channel Type -->
-       <channel-group-type id="smartPlug">
-               <label>SmartPlug</label>
-               <description>Tapo Smart Plug Power Outlet</description>
-               <channels>
-                       <channel id="output" typeId="outputChannel"/>
-               </channels>
-       </channel-group-type>
-
-       <channel-group-type id="powerStrip">
-               <label>SmartPlug</label>
-               <description>Tapo Smart Plug Power Outlet</description>
-               <channels>
-                       <channel id="output1" typeId="outputChannel">
-                               <label>Output Switch 1</label>
-                               <description>Switches the power state on/off of the first socket</description>
-                       </channel>
-                       <channel id="output2" typeId="outputChannel">
-                               <label>Output Switch 2</label>
-                               <description>Switches the power state on/off of the second socket</description>
-                       </channel>
-                       <channel id="output3" typeId="outputChannel">
-                               <label>Output Switch 3</label>
-                               <description>Switches the power state on/off of the third socket</description>
-                       </channel>
-               </channels>
-       </channel-group-type>
-
-       <!--Light-Bulb Channel Type -->
-       <channel-group-type id="lightBulb">
-               <label>Light Bulb</label>
-               <description>Tapo Smart Light Bulb</description>
-               <channels>
-                       <channel id="output" typeId="lightOn"/>
-                       <channel id="brightness" typeId="dimmerChannel"/>
-                       <channel id="colorTemperature" typeId="colorTemperature"/>
-               </channels>
-       </channel-group-type>
-
-       <!--Color Channel Type -->
-       <channel-group-type id="colorBulb">
-               <label>Color Light Bulb</label>
-               <description>Tapo Multicolor Smart Light Bulb</description>
-               <channels>
-                       <channel id="output" typeId="lightOn"/>
-                       <channel id="brightness" typeId="dimmerChannel"/>
-                       <channel id="color" typeId="colorChannel"/>
-                       <channel id="colorTemperature" typeId="colorTemperature"/>
-               </channels>
-       </channel-group-type>
-
-       <!-- LightStrip -->
-       <channel-group-type id="lightStrip">
-               <label>Color Light Strip</label>
-               <description>Tapo Multicolor Smart Light Strip</description>
-               <channels>
-                       <channel id="output" typeId="lightOn"/>
-                       <channel id="brightness" typeId="dimmerChannel"/>
-                       <channel id="color" typeId="colorChannel"/>
-                       <channel id="colorTemperature" typeId="colorTemperature"/>
-               </channels>
-       </channel-group-type>
-
-       <!-- Energy Monitor -->
-       <channel-group-type id="energyMonitor">
-               <label>Energy Usage</label>
-               <description>Energy and Power usage</description>
-               <channels>
-                       <channel id="actualPower" typeId="actualPowerChannel"></channel>
-                       <channel id="todayEnergyUsage" typeId="todayEnergyUsageChannel"></channel>
-                       <channel id="todayRuntime" typeId="todayRuntimeChannel"></channel>
-               </channels>
-       </channel-group-type>
-
        <!-- ############################### CHANNELS ############################### -->
 
        <!-- ACTOR CHANNEL TYPES -->
                <state readOnly="false"/>
        </channel-type>
 
+       <!-- Color Bulb Mode -->
+       <channel-type id="colorBulbMode">
+               <item-type>String</item-type>
+               <label>Mode</label>
+               <description>Working mode of device</description>
+               <category>LightBulb</category>
+               <state readOnly="false"/>
+               <command>
+                       <options>
+                               <option value="WHITE_LIGHT">White</option>
+                               <option value="COLOR_LIGHT">Color</option>
+                               <option value="LIGHT_FX">Effects</option>
+                       </options>
+               </command>
+       </channel-type>
+
        <!-- Color Channel Type -->
        <channel-type id="colorChannel">
                <item-type>Color</item-type>
        <channel-type id="colorTemperature">
                <item-type>Number</item-type>
                <label>Color Temperature</label>
-               <description>This channel supports adjusting the color temperature from 2700K to 6500K.</description>
+               <description>This channel supports adjusting the color temperature from 2200K to 6500K.</description>
                <category>LightBulb</category>
-               <state min="2500" max="6500" pattern="%d K"/>
+               <state min="2200" max="6500" pattern="%d K"/>
+       </channel-type>
+
+       <!-- SENSOR CHANNEL TYPES -->
+       <!-- SmartContact "isOpen" Channel Type -->
+       <channel-type id="isOpenChannel">
+               <item-type>Switch</item-type>
+               <label>Is Open</label>
+               <description>Contact (Window/Door) is Open</description>
+               <category>Switch</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <!-- Temperature Channel Type -->
+       <channel-type id="temperatureChannel">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>Temperature as measured by the sensor</description>
+               <category>Temperature</category>
+               <state readOnly="true" pattern="%.1f %unit%" step="0.1"/>
+       </channel-type>
+
+       <!-- Humidity Channel Type -->
+       <channel-type id="humidityChannel">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <description>Current relative humidity</description>
+               <category>Humidity</category>
+               <state readOnly="true" pattern="%.0f %%"/>
        </channel-type>
 
 
                <state readOnly="true"/>
        </channel-type>
 
+       <!-- is Online -->
+       <channel-type id="isOnlineChannel">
+               <item-type>Switch</item-type>
+               <label>Is Online</label>
+               <description>Device is Online</description>
+               <state readOnly="true"></state>
+       </channel-type>
 
-       <!-- DEVICE-STATE CHANNEL TYPES -->
+       <!-- battery Level -->
+       <channel-type id="batteryLowChannel">
+               <item-type>Switch</item-type>
+               <label>Battery Low</label>
+               <description>Battery of device is low</description>
+               <category>LowBattery</category>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <!-- ENERGY USAGE CHANNEL TYPES -->
        <!-- actual power usage -->
        <channel-type id="actualPowerChannel">
                <item-type>Number:Power</item-type>
                <state readOnly="true" pattern="%.0f %unit%"></state>
        </channel-type>
 
+       <!-- month energy usage -->
+       <channel-type id="monthEnergyUsageChannel">
+               <item-type>Number:Energy</item-type>
+               <label>Month Usage</label>
+               <description>Energy usage last month</description>
+               <category>Energy</category>
+               <state readOnly="true" pattern="%.2f %unit%"></state>
+       </channel-type>
+
+       <!-- month runtime -->
+       <channel-type id="monthRuntimeChannel">
+               <item-type>Number:Time</item-type>
+               <label>Month Runtime</label>
+               <description>Runtime last month (On-Time)</description>
+               <category>Time</category>
+               <state readOnly="true" pattern="%.0f %unit%"></state>
+       </channel-type>
+
        <!-- ADVANCED SETTING CHANNELS -->
        <!-- device led -->
        <channel-type id="led" advanced="true">
                <category>Switch</category>
        </channel-type>
 
+       <!-- ALARM CHANNEL CHANNELS -->
+       <!-- has active alarm -->
+       <channel-type id="alarmIsActiveChannel">
+               <item-type>Switch</item-type>
+               <label>Active Alarm</label>
+               <description>Device has an active alarm</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <!-- active alarm source -->
+       <channel-type id="alarmSourceChannel">
+               <item-type>String</item-type>
+               <label>Alarm Source</label>
+               <description>Source of active alarm</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <!-- TRIGGER CHANNELS -->
+       <!-- contact opened -->
+       <channel-type id="batteryLowEvent">
+               <kind>Trigger</kind>
+               <label>Battery Low</label>
+               <description>Battery state has changed to low. Replace Battery</description>
+       </channel-type>
+       <!-- contact opened -->
+       <channel-type id="contactOpenEvent">
+               <kind>Trigger</kind>
+               <label>Contact Opened</label>
+               <description>Event is fired if contact changes from closed to open</description>
+       </channel-type>
+       <!-- contact closed -->
+       <channel-type id="contactCloseEvent">
+               <kind>Trigger</kind>
+               <label>Contact Closed</label>
+               <description>Event is fired if contact changes from open to closed</description>
+       </channel-type>
+
 </thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/aurora.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/aurora.json
new file mode 100644 (file)
index 0000000..5edbf01
--- /dev/null
@@ -0,0 +1,73 @@
+ {
+  "id": "TapoStrip_1MClvV18i15Jq3bvJVf0eP",
+  "name": "Aurora",
+  "brightness": 100,
+  "display_colors": [
+    [
+      120,
+      100,
+      100
+    ],
+    [
+      240,
+      100,
+      100
+    ],
+    [
+      260,
+      100,
+      100
+    ],
+    [
+      280,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 4,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 0,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      120,
+      100,
+      100
+    ],
+    [
+      240,
+      100,
+      100
+    ],
+    [
+      260,
+      100,
+      100
+    ],
+    [
+      280,
+      100,
+      100
+    ]
+  ],
+  "spread": 7,
+  "transition": 1500,
+  "transition_range": null,
+  "type": "sequence",
+  "trans_sequence": null,
+  "run_time": null
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/bubbling_calderon.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/bubbling_calderon.json
new file mode 100644 (file)
index 0000000..bb381da
--- /dev/null
@@ -0,0 +1,63 @@
+ {
+  "id": "TapoStrip_6DlumDwO2NdfHppy50vJtu",
+  "name": "Bubbling Cauldron",
+  "brightness": 100,
+  "display_colors": [
+    [
+      100,
+      100,
+      100
+    ],
+    [
+      270,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    50,
+    100
+  ],
+  "backgrounds": [
+    [
+      270,
+      40,
+      50
+    ]
+  ],
+  "custom": 0,
+  "direction": null,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": 1000,
+  "hue_range": [
+    100,
+    270
+  ],
+  "init_states": [
+    [
+      270,
+      100,
+      100
+    ]
+  ],
+  "random_seed": 24,
+  "repeat_times": null,
+  "saturation_range": [
+    80,
+    100
+  ],
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 200,
+  "transition_range": null,
+  "type": "random",
+  "trans_sequence": null,
+  "run_time": null
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/candy_cane.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/candy_cane.json
new file mode 100644 (file)
index 0000000..213001d
--- /dev/null
@@ -0,0 +1,136 @@
+{
+    "id": "TapoStrip_6Dy0Nc45vlhFPEzG021Pe9",
+    "name": "Candy Cane",
+    "brightness": 100,
+    "display_colors": [
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ]
+    ],
+    "enable": 1,
+    "bAdjusted": null,
+    "brightness_range": [],
+    "backgrounds": [],
+    "custom": 0,
+    "direction": 1,
+    "duration": 700,
+    "expansion_strategy": 1,
+    "fadeoff": null,
+    "hue_range": null,
+    "init_states": [],
+    "random_seed": null,
+    "repeat_times": 0,
+    "saturation_range": null,
+    "segment_length": null,
+    "segments": [
+      0,
+      1,
+      2,
+      3,
+      4,
+      5,
+      6,
+      7,
+      8,
+      9,
+      10,
+      11,
+      12,
+      13,
+      14,
+      15
+    ],
+    "sequence": [
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        0,
+        0,
+        100
+      ],
+      [
+        360,
+        81,
+        100
+      ]
+    ],
+    "spread": 1,
+    "transition": 500,
+    "transition_range": null,
+    "type": "sequence"
+  }
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas.json
new file mode 100644 (file)
index 0000000..4e8366e
--- /dev/null
@@ -0,0 +1,76 @@
+{
+    "id": "TapoStrip_5zkiG6avJ1IbhjiZbRlWvh",
+    "name": "Christmas",
+    "brightness": 100,
+    "display_colors": [
+      [
+        136,
+        98,
+        100
+      ],
+      [
+        350,
+        97,
+        100
+      ]
+    ],
+    "enable": 1,
+    "bAdjusted": null,
+    "brightness_range": [
+      50,
+      100
+    ],
+    "backgrounds": [
+      [
+        136,
+        98,
+        75
+      ],
+      [
+        136,
+        0,
+        0
+      ],
+      [
+        350,
+        0,
+        100
+      ],
+      [
+        350,
+        97,
+        94
+      ]
+    ],
+    "custom": 0,
+    "direction": null,
+    "duration": 5000,
+    "expansion_strategy": 1,
+    "fadeoff": 2000,
+    "hue_range": [
+      136,
+      146
+    ],
+    "init_states": [
+      [
+        136,
+        0,
+        100
+      ]
+    ],
+    "random_seed": 100,
+    "repeat_times": null,
+    "saturation_range": [
+      90,
+      100
+    ],
+    "segment_length": null,
+    "segments": [
+      0
+    ],
+    "sequence": null,
+    "spread": null,
+    "transition": 0,
+    "transition_range": null,
+    "type": "random"
+  }
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas_light.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas_light.json
new file mode 100644 (file)
index 0000000..2e1f042
--- /dev/null
@@ -0,0 +1,126 @@
+ {
+  "id": "TapoStrip_3Gk6CmXOXbjCiwz9iD543C",
+  "name": "Grandma's Christmas Lights",
+  "brightness": 100,
+  "display_colors": [
+    [
+      30,
+      100,
+      100
+    ],
+    [
+      240,
+      100,
+      100
+    ],
+    [
+      130,
+      100,
+      100
+    ],
+    [
+      0,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 1,
+  "duration": 5000,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 0,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      30,
+      100,
+      100
+    ],
+    [
+      30,
+      0,
+      0
+    ],
+    [
+      30,
+      0,
+      0
+    ],
+    [
+      240,
+      100,
+      100
+    ],
+    [
+      240,
+      0,
+      0
+    ],
+    [
+      240,
+      0,
+      0
+    ],
+    [
+      240,
+      0,
+      100
+    ],
+    [
+      240,
+      0,
+      0
+    ],
+    [
+      240,
+      0,
+      0
+    ],
+    [
+      130,
+      100,
+      100
+    ],
+    [
+      130,
+      0,
+      0
+    ],
+    [
+      130,
+      0,
+      0
+    ],
+    [
+      0,
+      100,
+      100
+    ],
+    [
+      0,
+      0,
+      0
+    ],
+    [
+      0,
+      0,
+      0
+    ]
+  ],
+  "spread": 1,
+  "transition": 100,
+  "transition_range": null,
+  "type": "sequence"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/dynamic_light_fx.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/dynamic_light_fx.json
new file mode 100644 (file)
index 0000000..997a09e
--- /dev/null
@@ -0,0 +1,12 @@
+[
+    {
+       "id":"L1",
+       "name":"Party",
+       "enable":true
+    },
+    {
+       "id":"L2",
+       "name":"Relax",
+       "enable":true
+    }
+ ]
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/flicker.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/flicker.json
new file mode 100644 (file)
index 0000000..9ee3b17
--- /dev/null
@@ -0,0 +1,58 @@
+ {
+  "id": "TapoStrip_4HVKmMc6vEzjm36jXaGwMs",
+  "name": "Flicker",
+  "brightness": 100,
+  "display_colors": [
+    [
+      30,
+      81,
+      100
+    ],
+    [
+      40,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    50,
+    100
+  ],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": null,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": [
+    30,
+    40
+  ],
+  "init_states": [
+    [
+      30,
+      81,
+      80
+    ]
+  ],
+  "random_seed": null,
+  "repeat_times": null,
+  "saturation_range": [
+    100,
+    100
+  ],
+  "segment_length": null,
+  "segments": [
+    1
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 0,
+  "transition_range": [
+    375,
+    500
+  ],
+  "type": "random"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/hanukkah.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/hanukkah.json
new file mode 100644 (file)
index 0000000..a7ee2c5
--- /dev/null
@@ -0,0 +1,53 @@
+ {
+  "id": "TapoStrip_2YTk4wramLKv5XZ9KFDVYm",
+  "name": "Hanukkah",
+  "brightness": 100,
+  "display_colors": [
+    [
+      200,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    50,
+    100
+  ],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": null,
+  "duration": 1500,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": [
+    200,
+    210
+  ],
+  "init_states": [
+    [
+      35,
+      81,
+      80
+    ]
+  ],
+  "random_seed": null,
+  "repeat_times": null,
+  "saturation_range": [
+    0,
+    100
+  ],
+  "segment_length": null,
+  "segments": [
+    1
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 0,
+  "transition_range": [
+    400,
+    500
+  ],
+  "type": "random"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/haunted_mansion.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/haunted_mansion.json
new file mode 100644 (file)
index 0000000..566eb69
--- /dev/null
@@ -0,0 +1,59 @@
+ {
+  "id": "TapoStrip_4rJ6JwC7I9st3tQ8j4lwlI",
+  "name": "Haunted Mansion",
+  "brightness": 100,
+  "display_colors": [
+    [
+      45,
+      10,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    0,
+    80
+  ],
+  "backgrounds": [
+    [
+      45,
+      10,
+      100
+    ]
+  ],
+  "custom": 0,
+  "direction": null,
+  "duration": 0,
+  "expansion_strategy": 2,
+  "fadeoff": 200,
+  "hue_range": [
+    45,
+    45
+  ],
+  "init_states": [
+    [
+      45,
+      10,
+      100
+    ]
+  ],
+  "random_seed": 1,
+  "repeat_times": null,
+  "saturation_range": [
+    10,
+    10
+  ],
+  "segment_length": null,
+  "segments": [
+    80
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 0,
+  "transition_range": [
+    50,
+    1500
+  ],
+  "type": "random"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/icicle.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/icicle.json
new file mode 100644 (file)
index 0000000..8f6f7c0
--- /dev/null
@@ -0,0 +1,61 @@
+ {
+  "id": "TapoStrip_7UcYLeJbiaxVIXCxr21tpx",
+  "name": "Icicle",
+  "brightness": 100,
+  "display_colors": [
+    [
+      190,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 4,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 0,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      190,
+      100,
+      70
+    ],
+    [
+      190,
+      100,
+      70
+    ],
+    [
+      190,
+      30,
+      50
+    ],
+    [
+      190,
+      100,
+      70
+    ],
+    [
+      190,
+      100,
+      70
+    ]
+  ],
+  "spread": 3,
+  "transition": 400,
+  "transition_range": null,
+  "type": "sequence"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/lightning.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/lightning.json
new file mode 100644 (file)
index 0000000..2ec582c
--- /dev/null
@@ -0,0 +1,91 @@
+ {
+  "id": "TapoStrip_7OGzfSfnOdhoO2ri4gOHWn",
+  "name": "Lightning",
+  "brightness": 100,
+  "display_colors": [
+    [
+      210,
+      10,
+      100
+    ],
+    [
+      200,
+      50,
+      100
+    ],
+    [
+      200,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    90,
+    100
+  ],
+  "backgrounds": [
+    [
+      200,
+      100,
+      100
+    ],
+    [
+      200,
+      50,
+      10
+    ],
+    [
+      210,
+      10,
+      50
+    ],
+    [
+      240,
+      10,
+      0
+    ]
+  ],
+  "custom": 0,
+  "direction": null,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": 150,
+  "hue_range": [
+    240,
+    240
+  ],
+  "init_states": [
+    [
+      240,
+      30,
+      100
+    ]
+  ],
+  "random_seed": 600,
+  "repeat_times": null,
+  "saturation_range": [
+    10,
+    11
+  ],
+  "segment_length": null,
+  "segments": [
+    7,
+    20,
+    23,
+    32,
+    34,
+    35,
+    49,
+    65,
+    66,
+    74,
+    80
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 50,
+  "transition_range": null,
+  "type": "random"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/ocean.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/ocean.json
new file mode 100644 (file)
index 0000000..7ddae1d
--- /dev/null
@@ -0,0 +1,51 @@
+ {
+  "id": "TapoStrip_0fOleCdwSgR0nfjkReeYfw",
+  "name": "Ocean",
+  "brightness": 100,
+  "display_colors": [
+    [
+      198,
+      84,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 3,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 0,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      198,
+      84,
+      30
+    ],
+    [
+      198,
+      70,
+      30
+    ],
+    [
+      198,
+      10,
+      30
+    ]
+  ],
+  "spread": 16,
+  "transition": 2000,
+  "transition_range": null,
+  "type": "sequence"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/rainbow.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/rainbow.json
new file mode 100644 (file)
index 0000000..f33f69a
--- /dev/null
@@ -0,0 +1,71 @@
+ {
+  "id": "TapoStrip_7CC5y4lsL8pETYvmz7UOpQ",
+  "name": "Rainbow",
+  "brightness": 100,
+  "display_colors": [
+    [
+      0,
+      100,
+      100
+    ],
+    [
+      100,
+      100,
+      100
+    ],
+    [
+      200,
+      100,
+      100
+    ],
+    [
+      300,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 1,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 0,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      0,
+      100,
+      100
+    ],
+    [
+      100,
+      100,
+      100
+    ],
+    [
+      200,
+      100,
+      100
+    ],
+    [
+      300,
+      100,
+      100
+    ]
+  ],
+  "spread": 12,
+  "transition": 1500,
+  "transition_range": null,
+  "type": "sequence"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/raindrop.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/raindrop.json
new file mode 100644 (file)
index 0000000..aaae0dc
--- /dev/null
@@ -0,0 +1,61 @@
+ {
+  "id": "TapoStrip_1t2nWlTBkV8KXBZ0TWvBjs",
+  "name": "Raindrop",
+  "brightness": 100,
+  "display_colors": [
+    [
+      200,
+      10,
+      100
+    ],
+    [
+      200,
+      20,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    10,
+    30
+  ],
+  "backgrounds": [
+    [
+      200,
+      40,
+      0
+    ]
+  ],
+  "custom": 0,
+  "direction": null,
+  "duration": 0,
+  "expansion_strategy": 1,
+  "fadeoff": 1000,
+  "hue_range": [
+    200,
+    200
+  ],
+  "init_states": [
+    [
+      200,
+      40,
+      100
+    ]
+  ],
+  "random_seed": 24,
+  "repeat_times": null,
+  "saturation_range": [
+    10,
+    20
+  ],
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 1000,
+  "transition_range": null,
+  "type": "random"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/spring.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/spring.json
new file mode 100644 (file)
index 0000000..c6d292d
--- /dev/null
@@ -0,0 +1,64 @@
+ {
+  "id": "TapoStrip_1nL6GqZ5soOxj71YDJOlZL",
+  "name": "Spring",
+  "brightness": 100,
+  "display_colors": [
+    [
+      0,
+      30,
+      100
+    ],
+    [
+      130,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    90,
+    100
+  ],
+  "backgrounds": [
+    [
+      130,
+      100,
+      40
+    ]
+  ],
+  "custom": 0,
+  "direction": null,
+  "duration": 600,
+  "expansion_strategy": 1,
+  "fadeoff": 1000,
+  "hue_range": [
+    0,
+    90
+  ],
+  "init_states": [
+    [
+      80,
+      30,
+      100
+    ]
+  ],
+  "random_seed": 20,
+  "repeat_times": null,
+  "saturation_range": [
+    30,
+    100
+  ],
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 0,
+  "transition_range": [
+    2000,
+    6000
+  ],
+  "type": "random"
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunrise.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunrise.json
new file mode 100644 (file)
index 0000000..9b91f5c
--- /dev/null
@@ -0,0 +1,128 @@
+ {
+  "id": "TapoStrip_1OVSyXIsDxrt4j7OxyRvqi",
+  "name": "Sunrise",
+  "brightness": 100,
+  "display_colors": [
+    [
+      30,
+      0,
+      100
+    ],
+    [
+      30,
+      95,
+      100
+    ],
+    [
+      0,
+      100,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 1,
+  "duration": 600,
+  "expansion_strategy": 2,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 1,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      0,
+      100,
+      5
+    ],
+    [
+      0,
+      100,
+      5
+    ],
+    [
+      10,
+      100,
+      6
+    ],
+    [
+      15,
+      100,
+      7
+    ],
+    [
+      20,
+      100,
+      8
+    ],
+    [
+      20,
+      100,
+      10
+    ],
+    [
+      30,
+      100,
+      12
+    ],
+    [
+      30,
+      95,
+      15
+    ],
+    [
+      30,
+      90,
+      20
+    ],
+    [
+      30,
+      80,
+      25
+    ],
+    [
+      30,
+      75,
+      30
+    ],
+    [
+      30,
+      70,
+      40
+    ],
+    [
+      30,
+      60,
+      50
+    ],
+    [
+      30,
+      50,
+      60
+    ],
+    [
+      30,
+      20,
+      70
+    ],
+    [
+      30,
+      0,
+      100
+    ]
+  ],
+  "spread": 1,
+  "transition": 60000,
+  "transition_range": null,
+  "type": "pulse",
+  "trans_sequence": [],
+  "run_time": 0
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunset.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunset.json
new file mode 100644 (file)
index 0000000..84a2df0
--- /dev/null
@@ -0,0 +1,128 @@
+ {
+  "id": "TapoStrip_5NiN0Y8GAUD78p4neKk9EL",
+  "name": "Sunset",
+  "brightness": 100,
+  "display_colors": [
+    [
+      0,
+      100,
+      100
+    ],
+    [
+      30,
+      95,
+      100
+    ],
+    [
+      30,
+      0,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [],
+  "backgrounds": [],
+  "custom": 0,
+  "direction": 1,
+  "duration": 600,
+  "expansion_strategy": 2,
+  "fadeoff": null,
+  "hue_range": null,
+  "init_states": [],
+  "random_seed": null,
+  "repeat_times": 1,
+  "saturation_range": null,
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": [
+    [
+      30,
+      0,
+      100
+    ],
+    [
+      30,
+      20,
+      100
+    ],
+    [
+      30,
+      50,
+      99
+    ],
+    [
+      30,
+      60,
+      98
+    ],
+    [
+      30,
+      70,
+      97
+    ],
+    [
+      30,
+      75,
+      95
+    ],
+    [
+      30,
+      80,
+      93
+    ],
+    [
+      30,
+      90,
+      90
+    ],
+    [
+      30,
+      95,
+      85
+    ],
+    [
+      30,
+      100,
+      80
+    ],
+    [
+      20,
+      100,
+      70
+    ],
+    [
+      20,
+      100,
+      60
+    ],
+    [
+      15,
+      100,
+      50
+    ],
+    [
+      10,
+      100,
+      40
+    ],
+    [
+      0,
+      100,
+      30
+    ],
+    [
+      0,
+      100,
+      0
+    ]
+  ],
+  "spread": 1,
+  "transition": 60000,
+  "transition_range": null,
+  "type": "pulse",
+  "trans_sequence": [],
+  "run_time": 0
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/valentines.json b/bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/valentines.json
new file mode 100644 (file)
index 0000000..61b2429
--- /dev/null
@@ -0,0 +1,83 @@
+ {
+  "id": "TapoStrip_2q1Vio9sSjHmaC7JS9d30l",
+  "name": "Valentines",
+  "brightness": 100,
+  "display_colors": [
+    [
+      340,
+      20,
+      100
+    ],
+    [
+      20,
+      50,
+      100
+    ],
+    [
+      0,
+      100,
+      100
+    ],
+    [
+      340,
+      40,
+      100
+    ]
+  ],
+  "enable": 1,
+  "bAdjusted": null,
+  "brightness_range": [
+    90,
+    100
+  ],
+  "backgrounds": [
+    [
+      340,
+      20,
+      50
+    ],
+    [
+      20,
+      50,
+      50
+    ],
+    [
+      0,
+      100,
+      50
+    ]
+  ],
+  "custom": 0,
+  "direction": null,
+  "duration": 600,
+  "expansion_strategy": 1,
+  "fadeoff": 3000,
+  "hue_range": [
+    340,
+    340
+  ],
+  "init_states": [
+    [
+      340,
+      30,
+      100
+    ]
+  ],
+  "random_seed": 100,
+  "repeat_times": null,
+  "saturation_range": [
+    30,
+    40
+  ],
+  "segment_length": null,
+  "segments": [
+    0
+  ],
+  "sequence": null,
+  "spread": null,
+  "transition": 2000,
+  "transition_range": null,
+  "type": "random",
+  "trans_sequence": null,
+  "run_time": null
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java
deleted file mode 100644 (file)
index 2931bb5..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
-import org.openhab.core.config.discovery.AbstractDiscoveryService;
-import org.openhab.core.config.discovery.DiscoveryResult;
-import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-import org.openhab.core.thing.binding.ThingHandler;
-import org.openhab.core.thing.binding.ThingHandlerService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home thing discovery
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
-    private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
-    protected @NonNullByDefault({}) TapoBridgeHandler bridge;
-
-    /***********************************
-     *
-     * INITIALIZATION
-     *
-     ************************************/
-
-    /**
-     * INIT CLASS
-     * 
-     * @param bridgeHandler
-     */
-    public TapoDiscoveryService() {
-        super(SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
-    }
-
-    /**
-     * activate
-     */
-    @Override
-    public void activate() {
-        TapoBridgeConfiguration config = bridge.getBridgeConfig();
-        if (config.cloudDiscovery || config.udpDiscovery) {
-            startBackgroundDiscovery();
-        }
-    }
-
-    /**
-     * deactivate
-     */
-    @Override
-    public void deactivate() {
-        super.deactivate();
-    }
-
-    @Override
-    public void setThingHandler(@Nullable ThingHandler handler) {
-        if (handler instanceof TapoBridgeHandler tapoBridge) {
-            tapoBridge.setDiscoveryService(this);
-            this.bridge = tapoBridge;
-        }
-    }
-
-    @Override
-    public @Nullable ThingHandler getThingHandler() {
-        return this.bridge;
-    }
-
-    /***********************************
-     *
-     * SCAN HANDLING
-     *
-     ************************************/
-
-    /**
-     * Start scan manually
-     */
-    @Override
-    public void startScan() {
-        removeOlderResults(getTimestampOfLastScan());
-        if (bridge != null) {
-            JsonArray jsonArray = bridge.getDeviceList();
-            handleCloudDevices(jsonArray);
-        }
-    }
-
-    /***********************************
-     *
-     * handle Results
-     *
-     ************************************/
-
-    /**
-     * CREATE DISCOVERY RESULT
-     * creates discoveryResult (Thing) from JsonObject got from Cloud
-     * 
-     * @param device JsonObject with device information
-     * @return DiscoveryResult-Object
-     */
-    public DiscoveryResult createResult(JsonObject device) {
-        TapoBridgeHandler tapoBridge = this.bridge;
-        String deviceModel = getDeviceModel(device);
-        String label = getDeviceLabel(device);
-        String deviceMAC = device.get(CLOUD_JSON_KEY_MAC).getAsString();
-        ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
-        /* create properties */
-        Map<String, Object> properties = new HashMap<>();
-        properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
-        properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(deviceMAC, MAC_DIVISION_CHAR));
-        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.get(CLOUD_JSON_KEY_FW).getAsString());
-        properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.get(CLOUD_JSON_KEY_HW).getAsString());
-        properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
-        properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.get(CLOUD_JSON_KEY_ID).getAsString());
-
-        logger.debug("device {} discovered", deviceModel);
-        if (tapoBridge != null) {
-            ThingUID bridgeUID = tapoBridge.getUID();
-            ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
-            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
-                    .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
-                    .build();
-        } else {
-            ThingUID thingUID = new ThingUID(BINDING_ID, deviceMAC);
-            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
-                    .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withLabel(label).build();
-        }
-    }
-
-    /**
-     * work with result from get devices from cloud devices
-     * 
-     * @param deviceList
-     */
-    protected void handleCloudDevices(JsonArray deviceList) {
-        try {
-            for (JsonElement deviceElement : deviceList) {
-                if (deviceElement.isJsonObject()) {
-                    JsonObject device = deviceElement.getAsJsonObject();
-                    String deviceModel = getDeviceModel(device);
-                    ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
-                    /* create thing */
-                    if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
-                        DiscoveryResult discoveryResult = createResult(device);
-                        thingDiscovered(discoveryResult);
-                    }
-                }
-            }
-        } catch (Exception e) {
-            logger.debug("error handlling CloudDevices", e);
-        }
-    }
-
-    /**
-     * GET DEVICEMODEL
-     * 
-     * @param device JsonObject with deviceData
-     * @return String with DeviceModel
-     */
-    protected String getDeviceModel(JsonObject device) {
-        try {
-            String deviceModel = device.get(CLOUD_JSON_KEY_MODEL).getAsString();
-            deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
-            deviceModel = deviceModel.replace("Tapo", "");
-            deviceModel = deviceModel.replace("Series", "");
-            deviceModel = deviceModel.trim();
-            deviceModel = deviceModel.replace(" ", "_");
-            return deviceModel;
-        } catch (Exception e) {
-            logger.debug("error getDeviceModel", e);
-            return "";
-        }
-    }
-
-    /**
-     * GET DEVICE LABEL
-     * 
-     * @param device JsonObject with deviceData
-     * @return String with DeviceLabel
-     */
-    protected String getDeviceLabel(JsonObject device) {
-        try {
-            String deviceLabel = "";
-            String deviceModel = getDeviceModel(device);
-            ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
-            if (SUPPORTED_SMART_PLUG_UIDS.contains(deviceUID)) {
-                deviceLabel = DEVICE_DESCRIPTION_SMART_PLUG;
-            } else if (SUPPORTED_WHITE_BULB_UIDS.contains(deviceUID)) {
-                deviceLabel = DEVICE_DESCRIPTION_WHITE_BULB;
-            } else if (SUPPORTED_COLOR_BULB_UIDS.contains(deviceUID)) {
-                deviceLabel = DEVICE_DESCRIPTION_COLOR_BULB;
-            }
-            return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
-        } catch (Exception e) {
-            logger.debug("error getDeviceLabel", e);
-            return "";
-        }
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java
deleted file mode 100644 (file)
index 254eb5e..0000000
+++ /dev/null
@@ -1,317 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import java.util.Collection;
-import java.util.Set;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpClient;
-import org.openhab.binding.tapocontrol.internal.TapoDiscoveryService;
-import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
-import org.openhab.binding.tapocontrol.internal.api.TapoUDP;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
-import org.openhab.core.thing.Bridge;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.ThingUID;
-import org.openhab.core.thing.binding.BaseBridgeHandler;
-import org.openhab.core.thing.binding.ThingHandlerService;
-import org.openhab.core.types.Command;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-
-/**
- * The {@link TapoBridgeHandler} is responsible for handling commands, which are
- * sent to one of the channels with a bridge.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoBridgeHandler extends BaseBridgeHandler {
-    private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
-    private final TapoErrorHandler bridgeError = new TapoErrorHandler();
-    private final HttpClient httpClient;
-    private TapoBridgeConfiguration config;
-    private @Nullable ScheduledFuture<?> startupJob;
-    private @Nullable ScheduledFuture<?> pollingJob;
-    private @Nullable ScheduledFuture<?> discoveryJob;
-    private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
-    private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
-    private TapoCredentials credentials;
-
-    private String uid;
-
-    public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
-        super(bridge);
-        Thing thing = getThing();
-        this.cloudConnector = new TapoCloudConnector(this, httpClient);
-        this.config = new TapoBridgeConfiguration();
-        this.credentials = new TapoCredentials();
-        this.uid = thing.getUID().toString();
-        this.httpClient = httpClient;
-    }
-
-    /***********************************
-     *
-     * BRIDGE INITIALIZATION
-     *
-     ************************************/
-    @Override
-    /**
-     * INIT BRIDGE
-     * set credentials and login cloud
-     */
-    public void initialize() {
-        this.config = getConfigAs(TapoBridgeConfiguration.class);
-        this.credentials = new TapoCredentials(config.username, config.password);
-        activateBridge();
-    }
-
-    /**
-     * ACTIVATE BRIDGE
-     */
-    private void activateBridge() {
-        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
-        updateStatus(ThingStatus.UNKNOWN);
-
-        // background initialization (delay it a little bit):
-        this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
-    }
-
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
-    }
-
-    @Override
-    public void dispose() {
-        stopScheduler(this.startupJob);
-        stopScheduler(this.pollingJob);
-        stopScheduler(this.discoveryJob);
-        super.dispose();
-    }
-
-    /**
-     * ACTIVATE DISCOVERY SERVICE
-     */
-    @Override
-    public Collection<Class<? extends ThingHandlerService>> getServices() {
-        return Set.of(TapoDiscoveryService.class);
-    }
-
-    /**
-     * Set DiscoveryService
-     * 
-     * @param discoveryService
-     */
-    public void setDiscoveryService(TapoDiscoveryService discoveryService) {
-        this.discoveryService = discoveryService;
-    }
-
-    /***********************************
-     *
-     * SCHEDULER
-     *
-     ************************************/
-
-    /**
-     * delayed OneTime StartupJob
-     */
-    private void delayedStartUp() {
-        loginCloud();
-        startCloudScheduler();
-        startDiscoveryScheduler();
-    }
-
-    /**
-     * Start CloudLogin Scheduler
-     */
-    protected void startCloudScheduler() {
-        Integer pollingInterval = config.reconnectInterval;
-        if (pollingInterval > 0) {
-            logger.trace("{} starting bridge cloud sheduler", this.uid);
-
-            this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
-                    TimeUnit.MINUTES);
-        } else {
-            stopScheduler(this.pollingJob);
-        }
-    }
-
-    /**
-     * Start DeviceDiscovery Scheduler
-     */
-    protected void startDiscoveryScheduler() {
-        Integer pollingInterval = config.discoveryInterval;
-        if (config.cloudDiscovery && pollingInterval > 0) {
-            logger.trace("{} starting bridge discovery sheduler", this.uid);
-
-            this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval,
-                    TimeUnit.MINUTES);
-        } else {
-            stopScheduler(this.discoveryJob);
-        }
-    }
-
-    /**
-     * Stop scheduler
-     * 
-     * @param scheduler ScheduledFeature<?> which schould be stopped
-     */
-    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
-        if (scheduler != null) {
-            scheduler.cancel(true);
-            scheduler = null;
-        }
-    }
-
-    /***********************************
-     *
-     * ERROR HANDLER
-     *
-     ************************************/
-    /**
-     * return device Error
-     * 
-     * @return
-     */
-    public TapoErrorHandler getError() {
-        return this.bridgeError;
-    }
-
-    /**
-     * set device error
-     * 
-     * @param tapoError TapoErrorHandler-Object
-     */
-    public void setError(TapoErrorHandler tapoError) {
-        this.bridgeError.set(tapoError);
-    }
-
-    /***********************************
-     *
-     * BRIDGE COMMUNICATIONS
-     *
-     ************************************/
-
-    /**
-     * Login to Cloud
-     * 
-     * @return
-     */
-    public boolean loginCloud() {
-        bridgeError.reset(); // reset ErrorHandler
-        if (!config.username.isBlank() && !config.password.isBlank()) {
-            logger.debug("{} login with user {}", this.uid, config.username);
-            if (cloudConnector.login(config.username, config.password)) {
-                updateStatus(ThingStatus.ONLINE);
-                return true;
-            } else {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
-            }
-        } else {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set");
-        }
-        return false;
-    }
-
-    /***********************************
-     *
-     * DEVICE DISCOVERY
-     *
-     ************************************/
-
-    /**
-     * START DEVICE DISCOVERY
-     */
-    public void discoverDevices() {
-        this.discoveryService.startScan();
-    }
-
-    /**
-     * GET DEVICELIST CONNECTED TO BRIDGE
-     * 
-     * @return devicelist
-     */
-    public JsonArray getDeviceList() {
-        JsonArray deviceList = new JsonArray();
-        if (config.cloudDiscovery) {
-            logger.trace("{} discover devicelist from cloud", this.uid);
-            deviceList = getDeviceListCloud();
-        } else if (config.udpDiscovery) {
-            logger.trace("{} discover devicelist from udp", this.uid);
-            deviceList = getDeviceListUDP();
-        }
-        return deviceList;
-    }
-
-    /**
-     * GET DEVICELIST FROM CLOUD
-     * returns all devices stored in cloud
-     * 
-     * @return deviceList from cloud
-     */
-    private JsonArray getDeviceListCloud() {
-        logger.trace("{} getDeviceList from cloud", this.uid);
-        bridgeError.reset(); // reset ErrorHandler
-        JsonArray deviceList = new JsonArray();
-        if (loginCloud()) {
-            deviceList = this.cloudConnector.getDeviceList();
-        }
-        return deviceList;
-    }
-
-    /**
-     * GET DEVICELIST UDP
-     * return devices discovered by UDP
-     * 
-     * @return deviceList from udp
-     */
-    public JsonArray getDeviceListUDP() {
-        bridgeError.reset(); // reset ErrorHandler
-        TapoUDP udpDiscovery = new TapoUDP(credentials);
-        return udpDiscovery.udpScan();
-    }
-
-    /***********************************
-     *
-     * BRIDGE GETTERS
-     *
-     ************************************/
-
-    public TapoCredentials getCredentials() {
-        return this.credentials;
-    }
-
-    public HttpClient getHttpClient() {
-        return this.httpClient;
-    }
-
-    public ThingUID getUID() {
-        return getThing().getUID();
-    }
-
-    public TapoBridgeConfiguration getBridgeConfig() {
-        return this.config;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java
deleted file mode 100644 (file)
index 300c9eb..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Universal-Device
- * universal device for testing pruposes
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoUniversalDevice extends TapoDevice {
-    private final Logger logger = LoggerFactory.getLogger(TapoUniversalDevice.class);
-
-    // CHANNEL LIST
-    public static final String CHANNEL_GROUP_DEBUG = "debug";
-    public static final String CHANNEL_RESPONSE = "deviceResponse";
-    public static final String CHANNEL_COMMAND = "deviceCommand";
-
-    /**
-     * Constructor
-     *
-     * @param thing Thing object representing device
-     */
-    public TapoUniversalDevice(Thing thing) {
-        super(thing);
-    }
-
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command.toString(), channelUID.getId());
-        Boolean refreshInfo = false;
-
-        String channel = channelUID.getIdWithoutGroup();
-        if (command instanceof RefreshType) {
-            refreshInfo = true;
-        } else {
-            switch (channel) {
-                case CHANNEL_OUTPUT:
-                    connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
-                    refreshInfo = true;
-                    break;
-                case CHANNEL_BRIGHTNESS:
-                    if (command instanceof PercentType percentCommand) {
-                        Float percent = percentCommand.floatValue();
-                        setBrightness(percent.intValue()); // 0..100% = 0..100
-                        refreshInfo = true;
-                    } else if (command instanceof DecimalType decimalCommand) {
-                        setBrightness(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR_TEMP:
-                    if (command instanceof DecimalType decimalCommand) {
-                        setColorTemp(decimalCommand.intValue());
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COLOR:
-                    if (command instanceof HSBType hsbCommand) {
-                        setColor(hsbCommand);
-                        refreshInfo = true;
-                    }
-                    break;
-                case CHANNEL_COMMAND:
-                    String[] cmd = command.toString().split(":");
-                    if (cmd.length == 1) {
-                        connector.sendCustomQuery(cmd[0]);
-                    } else if (cmd.length == 2) {
-                        connector.sendDeviceCommand(cmd[0], cmd[1]);
-                    } else {
-                        logger.warn("({}) wrong command format '{}'", uid, command.toString());
-                    }
-                    break;
-                default:
-                    logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
-                            channelUID.getId());
-            }
-        }
-
-        /* refreshInfo */
-        if (refreshInfo) {
-            queryDeviceInfo();
-        }
-    }
-
-    /**
-     * SET BRIGHTNESS
-     * 
-     * @param newBrightness percentage 0-100 of new brightness
-     */
-    protected void setBrightness(Integer newBrightness) {
-        /* switch off if 0 */
-        if (newBrightness == 0) {
-            connector.sendDeviceCommand(JSON_KEY_ON, false);
-        } else {
-            HashMap<String, Object> newState = new HashMap<>();
-            newState.put(JSON_KEY_ON, true);
-            newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
-            connector.sendDeviceCommands(newState);
-        }
-    }
-
-    /**
-     * SET COLOR
-     * 
-     * @param command
-     */
-    protected void setColor(HSBType command) {
-        HashMap<String, Object> newState = new HashMap<>();
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_HUE, command.getHue());
-        newState.put(JSON_KEY_SATURATION, command.getSaturation());
-        newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness());
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * SET COLORTEMP
-     * 
-     * @param colorTemp (Integer) in Kelvin
-     */
-    protected void setColorTemp(Integer colorTemp) {
-        HashMap<String, Object> newState = new HashMap<>();
-        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
-        newState.put(JSON_KEY_ON, true);
-        newState.put(JSON_KEY_COLORTEMP, colorTemp);
-        connector.sendDeviceCommands(newState);
-    }
-
-    /**
-     * SET DEVICE INFOs to device
-     * 
-     * @param deviceInfo
-     */
-    @Override
-    public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
-        devicePropertiesChanged(deviceInfo);
-        handleConnectionState();
-    }
-
-    /**
-     * Handle full responsebody received from connector
-     * 
-     * @param responseBody
-     */
-    @Override
-    public void responsePasstrough(String responseBody) {
-        logger.info("({}) received response {}", uid, responseBody);
-        publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
-    }
-
-    /**
-     * UPDATE PROPERTIES
-     * 
-     * @param TapoDeviceInfo
-     */
-    @Override
-    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
-        super.devicePropertiesChanged(deviceInfo);
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
-                getPercentType(deviceInfo.getBrightness()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
-                getDecimalType(deviceInfo.getColorTemp()));
-        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
-
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
-                getDecimalType(deviceInfo.getSignalLevel()));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
-                getTimeType(deviceInfo.getOnTime(), Units.SECOND));
-        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
-                getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
-    }
-
-    /***********************************
-     *
-     * CHANNELS
-     *
-     ************************************/
-    /**
-     * Get ChannelID including group
-     * 
-     * @param group String channel-group
-     * @param channel String channel-name
-     * @return String channelID
-     */
-    @Override
-    protected String getChannelID(String group, String channel) {
-        return group + "#" + channel;
-    }
-
-    /**
-     * Get Channel from ChannelID
-     * 
-     * @param channelID String channelID
-     * @return String channel-name
-     */
-    @Override
-    protected String getChannelFromID(ChannelUID channelID) {
-        String channel = channelID.getIdWithoutGroup();
-        channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
-        channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
-        return channel;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoMDNS.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoMDNS.java
deleted file mode 100644 (file)
index 022f5ff..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.api;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-
-import java.util.Dictionary;
-import java.util.Set;
-
-import javax.jmdns.ServiceInfo;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.config.discovery.DiscoveryResult;
-import org.openhab.core.config.discovery.DiscoveryService;
-import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-import org.osgi.service.component.ComponentContext;
-import org.osgi.service.component.annotations.Activate;
-import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.Modified;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Handler class for TAPO Smart Home thing discovery over mDNS
- *
- * @author Christian Wild - Initial contribution
- */
-@Component(configurationPid = "discovery.tapocontrol")
-@NonNullByDefault
-public class TapoMDNS implements MDNSDiscoveryParticipant {
-    private final Logger logger = LoggerFactory.getLogger(TapoMDNS.class);
-    private boolean isAutoDiscoveryEnabled = true;
-
-    @Activate
-    protected void activate(ComponentContext componentContext) {
-        activateOrModifyService(componentContext);
-    }
-
-    @Modified
-    protected void modified(ComponentContext componentContext) {
-        activateOrModifyService(componentContext);
-    }
-
-    private void activateOrModifyService(ComponentContext componentContext) {
-        Dictionary<String, @Nullable Object> properties = componentContext.getProperties();
-        String autoDiscoveryPropertyValue = (String) properties
-                .get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY);
-        if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isBlank()) {
-            isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue);
-        }
-    }
-
-    @Override
-    public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
-        return SUPPORTED_THING_TYPES_UIDS;
-    }
-
-    @Override
-    public String getServiceType() {
-        return "NULL";
-    }
-
-    @Override
-    public @Nullable ThingUID getThingUID(ServiceInfo service) {
-        ThingTypeUID thingTypeUID = getThingType(service);
-        if (thingTypeUID != null) {
-            String id = service.getPropertyString(PROPERTY_FAMILY); // device id
-            return new ThingUID(thingTypeUID, id);
-        }
-        return null;
-    }
-
-    private @Nullable ThingTypeUID getThingType(final ServiceInfo service) {
-        String model = service.getPropertyString(PROPERTY_FAMILY); // model
-        logger.debug("found Type: {}", model);
-        if (model == null) {
-            return null;
-        }
-        return L510_THING_TYPE;
-    }
-
-    @Override
-    public @Nullable DiscoveryResult createResult(ServiceInfo service) {
-        if (isAutoDiscoveryEnabled) {
-            ThingUID uid = getThingUID(service);
-            if (uid != null) {
-                String host = service.getHostAddresses()[0];
-                int port = service.getPort();
-                logger.debug("device Found: {} {}", host, port);
-            }
-        }
-        return null;
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUDP.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUDP.java
deleted file mode 100644 (file)
index e76f0ea..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.api;
-
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.InetAddress;
-import java.net.SocketTimeoutException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.SecureRandom;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.interfaces.RSAPublicKey;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home device UDP-connections.
- * THIS IS FOR TESTING
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoUDP {
-    private final Logger logger = LoggerFactory.getLogger(TapoUDP.class);
-    private static final Integer BROADCAST_TIMEOUT_MS = 5000;
-    private static final Integer BROADCAST_DISCOVERY_PORT = 20002; // int
-    private static final String BROADCAST_IP = "255.255.255.255";
-    private static final String DISCOVERY_MESSAGE_KEY = "rsa_key";
-    private static final String DISCOVERY_MESSAGE_START_BYTES = "0200000101e5110001cb8c577dd7deb8";
-    private static final Integer BUFFER_SIZE = 501;
-    private TapoCredentials credentials;
-
-    public TapoUDP(TapoCredentials credentials) {
-        this.credentials = credentials; // new TapoCredentials();
-    }
-
-    public JsonArray udpScan() {
-        try {
-            DatagramSocket udpSocket = new DatagramSocket();
-            udpSocket.setSoTimeout(BROADCAST_TIMEOUT_MS);
-            udpSocket.setBroadcast(true);
-
-            /* create payload for handshake */
-            String publicKey = credentials.getPublicKey();
-            publicKey = generateOwnRSAKey(); // credentials.getPublicKey();
-            JsonObject parameters = new JsonObject();
-            JsonObject messageObject = new JsonObject();
-            parameters.addProperty(DISCOVERY_MESSAGE_KEY, publicKey);
-            messageObject.add("params", parameters);
-
-            String discoveryMessage = messageObject.toString();
-
-            byte[] startByte = hexStringToByteArray(DISCOVERY_MESSAGE_START_BYTES);
-            byte[] message = discoveryMessage.getBytes("UTF-8");
-            byte[] sendData = new byte[startByte.length + message.length];
-            System.arraycopy(startByte, 0, sendData, 0, startByte.length);
-            System.arraycopy(message, 0, sendData, startByte.length, message.length);
-
-            DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
-                    InetAddress.getByName(BROADCAST_IP), BROADCAST_DISCOVERY_PORT);
-
-            udpSocket.send(sendPacket);
-
-            while (true) {
-                // Wait for a response
-                byte[] recvBuf = new byte[BUFFER_SIZE];
-                DatagramPacket receivePacket;
-                try {
-                    receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
-                    udpSocket.receive(receivePacket);
-                } catch (SocketTimeoutException e) {
-                    udpSocket.close();
-                    return new JsonArray();
-                } catch (Exception e) {
-                    udpSocket.close();
-                    return new JsonArray();
-                }
-
-                // Check if the message is correct
-                String responseMessage = new String(receivePacket.getData(), "UTF-8").trim();
-
-                if (responseMessage.length() == 0) {
-                    udpSocket.close();
-                }
-                String addressBC = receivePacket.getAddress().getHostAddress();
-                gotDeviceAdress(addressBC);
-            }
-        } catch (Exception e) {
-            // handle exception
-        }
-        return new JsonArray();
-    }
-
-    private void gotDeviceAdress(String ipAddress) {
-        // handle exception
-    }
-
-    private String generateOwnRSAKey() {
-        try {
-            logger.trace("generating new keypair");
-            KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
-            instance.initialize(1536, new SecureRandom());
-            KeyPair generateKeyPair = instance.generateKeyPair();
-
-            String publicKey = new String(java.util.Base64.getMimeEncoder()
-                    .encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
-            String privateKey = new String(java.util.Base64.getMimeEncoder()
-                    .encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
-            logger.trace("new privateKey: '{}'", privateKey);
-            logger.trace("new ublicKey: '{}'", publicKey);
-
-            return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
-
-        } catch (Exception e) {
-            // couldn't generate own rsa key
-            return "";
-        }
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java
deleted file mode 100644 (file)
index f787f5c..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
- *
- * @author Christian Wild - Initial contribution
- */
-
-@NonNullByDefault
-public final class TapoBridgeConfiguration {
-    /* THING CONFIGUTATION PROPERTYS */
-    public static final String CONFIG_EMAIL = "username";
-    public static final String CONFIG_PASS = "password";
-    public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
-    public static final String CONFIG_DISCOVERY_UDP = "udpDiscovery";
-    public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
-
-    /* DEFAULT & FIXED CONFIGURATIONS */
-    public static final Integer CONFIG_CLOUD_FIXED_INTERVAL = 1440;
-
-    /* thing configuration parameter. */
-    public String username = "";
-    public String password = "";
-    public boolean cloudDiscovery = false;
-    public boolean udpDiscovery = false;
-    public int reconnectInterval = CONFIG_CLOUD_FIXED_INTERVAL;
-    public int discoveryInterval = 60;
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java
deleted file mode 100644 (file)
index 7a01c88..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.PercentType;
-
-import com.google.gson.JsonObject;
-
-/**
- * Tapo-Device Information class
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoDeviceInfo {
-    /**
-     * AVAILABLE BUT UNUSED FIELDS
-     * remove before push to real version
-     * 
-     * private Boolean hasSetLocationInfo = false;
-     * private Integer latitude = 0;
-     * private Integer longitude = 0;
-     * private Integer timeDiff = 0;
-     * private String avatar = "";
-     * private String fwId = "";
-     * private String hwId = "";
-     * private String specs = "";
-     * private String ssid = "";
-     * private String oemId = "";
-     * private String lang = "";
-     * private String location = "";
-     */
-
-    private Boolean deviceOn = false;
-    private Boolean overheated = false;
-    private Integer brightness = 0;
-    private Integer colorTemp = 0;
-    private Integer hue = 0;
-    private Integer rssi = 0;
-    private Integer saturation = 100;
-    private Integer signalLevel = 0;
-    private Number onTime = 0;
-    private Number timeUsagePast30 = 0;
-    private Number timeUsagePast7 = 0;
-    private Number timeUsageToday = 0;
-    private String deviceId = "";
-    private String fwVer = "";
-    private String hwVer = "";
-    private String ip = "";
-    private String mac = "";
-    private String model = "";
-    private String nickname = "";
-    private String region = "";
-    private String type = "";
-    private TapoLightEffect lightEffect = new TapoLightEffect();
-
-    private JsonObject jsonObject = new JsonObject();
-
-    /**
-     * INIT
-     */
-    public TapoDeviceInfo() {
-        setData();
-    }
-
-    /**
-     * Init DeviceInfo with new Data;
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoDeviceInfo(JsonObject jso) {
-        jsonObject = jso;
-        setData();
-    }
-
-    /**
-     * Set Data (new JsonObject)
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoDeviceInfo setData(JsonObject jso) {
-        this.jsonObject = jso;
-        setData();
-        return this;
-    }
-
-    private void setData() {
-        this.brightness = jsonObjectToInt(jsonObject, JSON_KEY_BRIGHTNESS);
-        this.colorTemp = jsonObjectToInt(jsonObject, JSON_KEY_COLORTEMP, BULB_MIN_COLORTEMP);
-        this.deviceId = jsonObjectToString(jsonObject, JSON_KEY_ID);
-        this.deviceOn = jsonObjectToBool(jsonObject, JSON_KEY_ON);
-        this.fwVer = jsonObjectToString(jsonObject, JSON_KEY_FW);
-        this.hue = jsonObjectToInt(jsonObject, JSON_KEY_HUE);
-        this.hwVer = jsonObjectToString(jsonObject, JSON_KEY_HW_VER);
-        this.ip = jsonObjectToString(jsonObject, JSON_KEY_IP);
-        this.lightEffect = lightEffect.setData(jsonObject);
-        this.mac = jsonObjectToString(jsonObject, JSON_KEY_MAC);
-        this.model = jsonObjectToString(jsonObject, JSON_KEY_MODEL);
-        this.nickname = jsonObjectToString(jsonObject, JSON_KEY_NICKNAME);
-        this.onTime = jsonObjectToNumber(jsonObject, JSON_KEY_ONTIME);
-        this.overheated = jsonObjectToBool(jsonObject, JSON_KEY_OVERHEAT);
-        this.region = jsonObjectToString(jsonObject, JSON_KEY_REGION);
-        this.saturation = jsonObjectToInt(jsonObject, JSON_KEY_SATURATION);
-        this.signalLevel = jsonObjectToInt(jsonObject, JSON_KEY_SIGNAL_LEVEL);
-        this.rssi = jsonObjectToInt(jsonObject, JSON_KEY_RSSI);
-        this.timeUsagePast7 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_7);
-        this.timeUsagePast30 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_30);
-        this.timeUsageToday = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_TODAY);
-        this.type = jsonObjectToString(jsonObject, JSON_KEY_TYPE);
-    }
-
-    /***********************************
-     *
-     * GET VALUES
-     *
-     ************************************/
-
-    public Integer getBrightness() {
-        return brightness;
-    }
-
-    public Integer getColorTemp() {
-        return colorTemp;
-    }
-
-    public String getFirmwareVersion() {
-        return fwVer;
-    }
-
-    public String getHardwareVersion() {
-        return hwVer;
-    }
-
-    public HSBType getHSB() {
-        DecimalType h = new DecimalType(hue);
-        PercentType s = new PercentType(saturation);
-        PercentType b = new PercentType(brightness);
-        return new HSBType(h, s, b);
-    }
-
-    public Integer getHue() {
-        return hue;
-    }
-
-    public TapoLightEffect getLightEffect() {
-        return lightEffect;
-    }
-
-    public String getIP() {
-        return ip;
-    }
-
-    public Boolean isOff() {
-        return !deviceOn;
-    }
-
-    public Boolean isOn() {
-        return deviceOn;
-    }
-
-    public Boolean isOverheated() {
-        return overheated;
-    }
-
-    public String getMAC() {
-        return formatMac(mac, MAC_DIVISION_CHAR);
-    }
-
-    public String getModel() {
-        return model.replace(" ", "_");
-    }
-
-    public String getNickname() {
-        return nickname;
-    }
-
-    public Number getOnTime() {
-        return onTime;
-    }
-
-    public String getRegion() {
-        return region;
-    }
-
-    public String getRepresentationProperty() {
-        return getMAC();
-    }
-
-    public Integer getSaturation() {
-        return saturation;
-    }
-
-    public String getSerial() {
-        return deviceId;
-    }
-
-    public Integer getSignalLevel() {
-        return signalLevel;
-    }
-
-    public Integer getRSSI() {
-        return rssi;
-    }
-
-    public Number getTimeUsagePast7() {
-        return timeUsagePast7;
-    }
-
-    public Number getTimeUsagePast30() {
-        return timeUsagePast30;
-    }
-
-    public Number getTimeUsagePastToday() {
-        return timeUsageToday;
-    }
-
-    public String getType() {
-        return type;
-    }
-
-    @Override
-    public String toString() {
-        return jsonObject.toString();
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java
deleted file mode 100644 (file)
index b857f15..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-/**
- * Copyright (c) 2010-2024 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.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.awt.Color;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.JsonObject;
-
-/**
- * Tapo-LightningEffect Structure Class
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoLightEffect {
-    private Integer enable = 0;
-    private String id = "";
-    private String name = "";
-    private Integer custom = 0;
-    private Integer brightness = 0;
-    private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
-    private Color[] displayColors = { Color.WHITE };
-
-    private JsonObject jsonObject = new JsonObject();
-
-    /**
-     * INIT
-     */
-    public TapoLightEffect() {
-        setData();
-    }
-
-    /**
-     * Init DeviceInfo with new Data;
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoLightEffect(JsonObject jso) {
-        setData(jso);
-    }
-
-    /**
-     * Set Data (new JsonObject)
-     * 
-     * @param jso JsonObject new Data
-     */
-    public TapoLightEffect setData(JsonObject jso) {
-        /* create empty jsonObject to set efault values if has no lighning effect */
-        if (jsonObject.has(JSON_KEY_LIGHTNING_EFFECT)) {
-            this.jsonObject = jso;
-        } else {
-            jsonObject = new JsonObject();
-        }
-        setData();
-        return this;
-    }
-
-    private void setData() {
-        this.enable = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ENABLE);
-        this.id = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ID);
-        this.name = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_NAME);
-        this.custom = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_CUSTOM); // jsonObjectToBool
-        this.brightness = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS);
-        // this.color_temp_range = { 9000, 9000 }; PROPERTY_LIGHNTING_ //:[9000,9000]
-        // this.displayColors[] PROPERTY_LIGHNTING_;
-    }
-
-    /***********************************
-     *
-     * SET VALUES
-     *
-     ************************************/
-
-    public void setEnable(Boolean enable) {
-        this.enable = enable ? 1 : 0;
-    }
-
-    public void setName(String value) {
-        this.name = value;
-    }
-
-    public void setCustom(Boolean enable) {
-        this.custom = enable ? 1 : 0;
-    }
-
-    public void setBrightness(Integer value) {
-        this.brightness = value;
-    }
-
-    public void setColorTempRange() {
-    }
-
-    public void setDisplayColors() {
-    }
-
-    /***********************************
-     *
-     * GET VALUES
-     *
-     ************************************/
-
-    public Integer getEnable() {
-        return this.enable;
-    }
-
-    public String getId() {
-        return this.id;
-    }
-
-    public String getName() {
-        return this.name;
-    }
-
-    public Integer getCustom() {
-        return this.custom;
-    }
-
-    public Integer getBrightness() {
-        return this.brightness;
-    }
-
-    public Integer[] getColorTempRange() {
-        return this.colorTempRange;
-    }
-
-    public Color[] getDisplayColors() {
-        return this.displayColors;
-    }
-
-    @Override
-    public String toString() {
-        return jsonObject.toString();
-    }
-}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/config/config.xml
deleted file mode 100644 (file)
index 8c516a8..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<config-description:config-descriptions
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
-       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
-
-       <config-description uri="thing-type:tapo:device">
-               <parameter name="ipAddress" type="text" required="true">
-                       <context>network-address</context>
-                       <label>IP Address</label>
-               </parameter>
-               <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
-                       <label>Refresh Interval</label>
-                       <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
-                       <default>30</default>
-                       <advanced>true</advanced>
-               </parameter>
-       </config-description>
-
-       <config-description uri="bridge-type:tapo:bridge">
-               <parameter name="username" type="text" required="true">
-                       <context>email</context>
-                       <label>Username</label>
-                       <description>Tapo-Cloud Login User (e-Mail)</description>
-               </parameter>
-               <parameter name="password" type="text" required="true">
-                       <context>password</context>
-                       <label>Password</label>
-                       <description>Tapo-Cloud Login Password</description>
-               </parameter>
-               <parameter name="cloudDiscovery" type="boolean" required="false">
-                       <label>Cloud Discovery</label>
-                       <description>Use Cloud Discovery-Service</description>
-                       <default>false</default>
-                       <advanced>false</advanced>
-               </parameter>
-               <parameter name="cloudReconnect" type="integer" min="0" max="10080" required="false">
-                       <label>Cloud Reconnect Interval</label>
-                       <description>Interval for reconnecting to the Tapo-Cloud in minutes (default 1440 = 24h / 0 = disabled)</description>
-                       <default>1440</default>
-                       <advanced>true</advanced>
-               </parameter>
-               <!--
-               <parameter name="discoveryInterval" type="integer" min="0" max="10080" required="false">
-                       <label>Background Discovery Interval</label>
-                       <description>Interval background discovery in minutes (default 60 / 0 = disabled)</description>
-                       <default>60</default>
-                       <advanced>true</advanced>
-               </parameter>
-               <parameter name="udpDiscovery" type="boolean" required="false">
-                       <label>UDP Discovery</label>
-                       <description>Use UDP Discovery-Service</description>
-                       <default>false</default>
-                       <advanced>true</advanced>
-               </parameter>
-               -->
-       </config-description>
-</config-description:config-descriptions>
index 2f6888355e96ab602e307920ca15f401e683f960..ee8c6317faf81dc5dcd40d335c46971f512a7af5 100644 (file)
@@ -16,7 +16,8 @@
                        <channel-group id="actuator" typeId="colorBulb"/>
                        <channel-group id="device" typeId="deviceState"/>
                        <channel-group id="effect" typeId="lightEffect"/>
-                       <channel-group id="debug" typeId="commandDebug"/>
+                       <channel-group id="response" typeId="responseChannels"/>
+                       <channel-group id="devicecommand" typeId="commandChannels"/>
                </channel-groups>
                <representation-property>macAddress</representation-property>
 
        <!-- ############################### CHANNEL-GROUPS ############################### -->
 
        <!-- CHANNEL GROUP TYPES -->
-       <!--Device-Statuss Channel Type -->
-       <channel-group-type id="commandDebug">
+       <!-- Command Debug Channel Type -->
+       <channel-group-type id="responseChannels" advanced="true">
                <label>Device Communication Debug</label>
                <description>Device resoponses and command debugging</description>
                <channels>
-                       <channel id="deviceResponse" typeId="deviceResponse"/>
                        <channel id="deviceCommand" typeId="deviceCommand"/>
+                       <channel id="deviceResponse" typeId="deviceResponse"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- Command Debug Channel Type -->
+       <channel-group-type id="commandChannels" advanced="true">
+               <label>Device Command Channels</label>
+               <description>sent commands to the device manually</description>
+               <channels>
+                       <channel id="method" typeId="deviceCommand"/>
+                       <channel id="params" typeId="deviceParams"/>
+                       <channel id="secure" typeId="securePasstrough"/>
+                       <channel id="send" typeId="sendSwitch"/>
                </channels>
        </channel-group-type>
 
        <!-- ############################### CHANNELS ############################### -->
 
-       <!-- OuputState Channel Type -->
+       <!-- response from device -->
        <channel-type id="deviceResponse">
                <item-type>String</item-type>
                <label>Device Response</label>
-               <description>DeviceResponse</description>
-               <state readOnly="true"/>
+               <description>DeviceResponse from device.</description>
+               <state readOnly="false"/>
        </channel-type>
 
-       <!-- OuputState Channel Type -->
+       <!-- device command -->
        <channel-type id="deviceCommand">
                <item-type>String</item-type>
                <label>Device Command</label>
-               <description>command send to device. use: 'command':'value'</description>
+               <description>command (method) to send to device</description>
+               <state readOnly="false">
+                       <options>
+                               <option value="setDeviceData">Set Data</option>
+                               <option value="queryInfo">Query DeviceInfo</option>
+                               <option value="getChildData">Get Child Data</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <!-- device params -->
+       <channel-type id="deviceParams">
+               <item-type>String</item-type>
+               <label>Params</label>
+               <description>params to send to device. Use JSON String</description>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- secure passtrough -->
+       <channel-type id="securePasstrough">
+               <item-type>Switch</item-type>
+               <label>Encrypt Message</label>
+               <description>encrypt message when sent to device</description>
                <state readOnly="false"/>
        </channel-type>
 
+       <!-- secure passtrough -->
+       <channel-type id="sendSwitch">
+               <item-type>Switch</item-type>
+               <label>Send</label>
+               <description>send command to device</description>
+               <state readOnly="false"/>
+       </channel-type>
+
+
 </thing:thing-descriptions>