From dbe970981c4d10eb94b8cb229518c76a84e31c67 Mon Sep 17 00:00:00 2001 From: Christian Wild <40909464+wildcs@users.noreply.github.com> Date: Sun, 28 Apr 2024 09:38:06 +0200 Subject: [PATCH] [tapocontrol] new communication protocol integration / code revision (#15725) * [tapocontrol] new tapo klap-protocol integration Signed-off-by: Christian Wild --- .../org.openhab.binding.tapocontrol/README.md | 100 ++- .../internal/TapoControlHandlerFactory.java | 41 +- .../internal/TapoDiscoveryService.java | 195 ------ .../internal/api/TapoCloudConnector.java | 287 ++++---- .../internal/api/TapoConnectorInterface.java | 45 ++ .../internal/api/TapoDeviceConnector.java | 627 +++++++++--------- .../internal/api/TapoDeviceHttpApi.java | 622 ----------------- .../api/protocol/TapoProtocolEnum.java | 49 ++ .../api/protocol/TapoProtocolInterface.java | 61 ++ .../api/protocol/aes/SecurePassthrough.java | 276 ++++++++ .../aes/SecurePassthroughSession.java | 270 ++++++++ .../protocol/aes/SecurePasstroughCipher.java} | 63 +- .../api/protocol/klap/KlapCipher.java | 200 ++++++ .../api/protocol/klap/KlapProtocol.java | 274 ++++++++ .../api/protocol/klap/KlapSession.java | 323 +++++++++ .../passthrough/PassthroughProtocol.java | 257 +++++++ .../constants/TapoBindingSettings.java | 17 +- .../internal/constants/TapoComConstants.java | 53 ++ .../internal/constants/TapoErrorCode.java | 19 +- .../constants/TapoThingConstants.java | 143 ++-- .../internal/device/TapoLightStrip.java | 225 ------- .../internal/device/TapoSmartBulb.java | 206 ------ .../internal/device/TapoSmartPlug.java | 96 --- .../internal/device/TapoUniversalDevice.java | 236 ------- .../bridge}/TapoBridgeConfiguration.java | 6 +- .../bridge}/TapoBridgeHandler.java | 135 ++-- .../bridge/dto/TapoCloudLoginData.java | 35 + .../bridge/dto/TapoCloudLoginResult.java | 75 +++ .../devices/dto/TapoBaseDeviceData.java | 159 +++++ .../devices/dto/TapoChildDeviceData.java | 202 ++++++ .../dto/TapoChildList.java} | 24 +- .../internal/devices/dto/TapoEnergyData.java | 91 +++ .../devices/dto/TapoLightDynamicFx.java | 138 ++++ .../internal/devices/dto/TapoLightEffect.java | 230 +++++++ .../devices/rf/TapoChildDeviceHandler.java | 212 ++++++ .../smartcontact/TapoSmartContactHandler.java | 68 ++ .../TapoWheaterSensorHandler.java | 51 ++ .../wifi/TapoBaseDeviceHandler.java} | 370 ++++++----- .../devices/wifi/TapoDeviceConfiguration.java | 61 ++ .../wifi/TapoUniversalDeviceHandler.java | 287 ++++++++ .../devices/wifi/bulb/TapoBulbData.java | 193 ++++++ .../devices/wifi/bulb/TapoBulbHandler.java | 263 ++++++++ .../devices/wifi/bulb/TapoBulbLastStates.java | 82 +++ .../wifi/bulb/TapoBulbModeEnum.java} | 18 +- .../devices/wifi/hub/TapoHubData.java | 48 ++ .../devices/wifi/hub/TapoHubHandler.java | 288 ++++++++ .../wifi/lightstrip/TapoLightStripData.java | 150 +++++ .../lightstrip/TapoLightStripHandler.java | 253 +++++++ .../devices/wifi/socket/TapoSocketData.java | 95 +++ .../wifi/socket/TapoSocketHandler.java | 122 ++++ .../wifi/socket/TapoSocketStripHandler.java | 178 +++++ .../discovery/TapoChildDiscoveryService.java | 205 ++++++ .../discovery/TapoDiscoveryService.java | 285 ++++++++ .../internal/discovery/TapoUdpDiscovery.java | 267 ++++++++ .../discovery/dto/TapoDiscoveryResult.java | 168 +++++ .../dto/TapoDiscoveryResultList.java | 113 ++++ .../dto/TapoBaseRequestInterface.java | 26 + .../internal/dto/TapoChildRequest.java | 60 ++ .../internal/dto/TapoMultipleRequest.java | 59 ++ .../tapocontrol/internal/dto/TapoRequest.java | 57 ++ .../internal/dto/TapoResponse.java | 101 +++ .../internal/helpers/MimeEncode.java | 42 -- .../internal/helpers/PayloadBuilder.java | 92 --- .../internal/helpers/TapoCredentials.java | 196 +----- .../internal/helpers/TapoEncoder.java | 108 +++ .../internal/helpers/TapoErrorHandler.java | 109 +-- .../internal/helpers/TapoKeyPair.java | 110 +++ .../internal/helpers/TapoUtils.java | 371 ----------- .../internal/helpers/utils/ByteUtils.java | 183 +++++ .../internal/helpers/utils/JsonUtils.java | 179 +++++ .../internal/helpers/utils/StringUtils.java | 79 +++ .../internal/helpers/utils/TapoUtils.java | 186 ++++++ .../internal/helpers/utils/TypeUtils.java | 162 +++++ .../structures/TapoBridgeConfiguration.java | 40 -- .../internal/structures/TapoChild.java | 140 ---- .../internal/structures/TapoDeviceInfo.java | 236 ------- .../internal/structures/TapoEnergyData.java | 129 ---- .../internal/structures/TapoLightEffect.java | 152 ----- .../internal/structures/TapoSubRequest.java | 44 -- .../resources/OH-INF/config/bridgeconfig.xml} | 52 +- .../main/resources/OH-INF/config/config.xml | 44 -- .../resources/OH-INF/config/deviceconfig.xml | 38 ++ .../resources/OH-INF/config/hubconfig.xml | 45 ++ .../OH-INF/i18n/tapocontrol.properties | 146 +++- .../src/main/resources/OH-INF/thing/H100.xml | 24 + .../src/main/resources/OH-INF/thing/L530.xml | 5 +- .../src/main/resources/OH-INF/thing/L900.xml | 39 ++ .../src/main/resources/OH-INF/thing/L920.xml | 8 +- .../src/main/resources/OH-INF/thing/L930.xml | 10 +- .../src/main/resources/OH-INF/thing/T110.xml | 21 + .../src/main/resources/OH-INF/thing/T310.xml | 22 + .../src/main/resources/OH-INF/thing/T315.xml | 22 + .../resources/OH-INF/thing/channelgroups.xml | 148 +++++ .../main/resources/OH-INF/thing/channels.xml | 221 +++--- .../main/resources/lightningfx/aurora.json | 73 ++ .../lightningfx/bubbling_calderon.json | 63 ++ .../resources/lightningfx/candy_cane.json | 136 ++++ .../main/resources/lightningfx/christmas.json | 76 +++ .../lightningfx/christmas_light.json | 126 ++++ .../lightningfx/dynamic_light_fx.json | 12 + .../main/resources/lightningfx/flicker.json | 58 ++ .../main/resources/lightningfx/hanukkah.json | 53 ++ .../lightningfx/haunted_mansion.json | 59 ++ .../main/resources/lightningfx/icicle.json | 61 ++ .../main/resources/lightningfx/lightning.json | 91 +++ .../src/main/resources/lightningfx/ocean.json | 51 ++ .../main/resources/lightningfx/rainbow.json | 71 ++ .../main/resources/lightningfx/raindrop.json | 61 ++ .../main/resources/lightningfx/spring.json | 64 ++ .../main/resources/lightningfx/sunrise.json | 128 ++++ .../main/resources/lightningfx/sunset.json | 128 ++++ .../resources/lightningfx/valentines.json | 83 +++ .../internal/TapoDiscoveryService.java | 230 ------- .../internal/device/TapoBridgeHandler.java | 317 --------- .../internal/device/TapoUniversalDevice.java | 236 ------- .../internal/discovery/TapoMDNS.java | 107 --- .../internal/discovery/TapoUDP.java | 138 ---- .../internal/structures/TapoDeviceInfo.java | 242 ------- .../internal/structures/TapoLightEffect.java | 149 ----- .../resources/OH-INF/thing/testdevice.xml | 62 +- 120 files changed, 10531 insertions(+), 5607 deletions(-) delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoConnectorInterface.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceHttpApi.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolEnum.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolInterface.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthrough.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthroughSession.java rename bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/{helpers/TapoCipher.java => api/protocol/aes/SecurePasstroughCipher.java} (62%) create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapCipher.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapProtocol.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapSession.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/passthrough/PassthroughProtocol.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoComConstants.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartBulb.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java rename bundles/org.openhab.binding.tapocontrol/src/{test/java/org/openhab/binding/tapocontrol/internal/structures => main/java/org/openhab/binding/tapocontrol/internal/devices/bridge}/TapoBridgeConfiguration.java (81%) rename bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/{device => devices/bridge}/TapoBridgeHandler.java (64%) create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginResult.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoBaseDeviceData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildDeviceData.java rename bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/{structures/TapoChildData.java => devices/dto/TapoChildList.java} (59%) create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoEnergyData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightDynamicFx.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightEffect.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/TapoChildDeviceHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/smartcontact/TapoSmartContactHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/wheatersensor/TapoWheaterSensorHandler.java rename bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/{device/TapoDevice.java => devices/wifi/TapoBaseDeviceHandler.java} (57%) create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoDeviceConfiguration.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoUniversalDeviceHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbLastStates.java rename bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/{structures/TapoDeviceConfiguration.java => devices/wifi/bulb/TapoBulbModeEnum.java} (50%) create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketData.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketStripHandler.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoChildDiscoveryService.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoDiscoveryService.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUdpDiscovery.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResult.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResultList.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoBaseRequestInterface.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoChildRequest.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoMultipleRequest.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoRequest.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoResponse.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/PayloadBuilder.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoEncoder.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoKeyPair.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoUtils.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/ByteUtils.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/JsonUtils.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/StringUtils.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TapoUtils.java create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/utils/TypeUtils.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChild.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoEnergyData.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoSubRequest.java rename bundles/org.openhab.binding.tapocontrol/src/{test/resources/OH-INF/config/config.xml => main/resources/OH-INF/config/bridgeconfig.xml} (57%) delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/deviceconfig.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/hubconfig.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/H100.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T110.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T310.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/T315.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channelgroups.xml create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/aurora.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/bubbling_calderon.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/candy_cane.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/christmas_light.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/dynamic_light_fx.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/flicker.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/hanukkah.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/haunted_mansion.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/icicle.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/lightning.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/ocean.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/rainbow.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/raindrop.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/spring.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunrise.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/sunset.json create mode 100644 bundles/org.openhab.binding.tapocontrol/src/main/resources/lightningfx/valentines.json delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoMDNS.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUDP.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java delete mode 100644 bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java diff --git a/bundles/org.openhab.binding.tapocontrol/README.md b/bundles/org.openhab.binding.tapocontrol/README.md index b429f3c469..0a458dbb72 100644 --- a/bundles/org.openhab.binding.tapocontrol/README.md +++ b/bundles/org.openhab.binding.tapocontrol/README.md @@ -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 ] diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoControlHandlerFactory.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoControlHandlerFactory.java index 855bcc9ecb..0238678c34 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoControlHandlerFactory.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoControlHandlerFactory.java @@ -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 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 index 8fe5df702a..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java +++ /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 { - 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 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 ""; - } - } -} diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoCloudConnector.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoCloudConnector.java index 0fdafd9ca7..1dcbb2ee21 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoCloudConnector.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoCloudConnector.java @@ -13,228 +13,185 @@ 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 index 0000000000..e2a8197e5f --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoConnectorInterface.java @@ -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(); +} diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceConnector.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceConnector.java index c8f88d0278..d5d2024a0b 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceConnector.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceConnector.java @@ -12,450 +12,459 @@ */ 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 (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 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 (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 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 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 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 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 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 getObjectFromJson(String json, Class 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 getObjectFromJson(JsonObject jso, Class 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 getResponseData(Class 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 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 index 4382b0d1fd..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceHttpApi.java +++ /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 index 0000000000..1d707449c7 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolEnum.java @@ -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 index 0000000000..469c88e043 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/TapoProtocolInterface.java @@ -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 index 0000000000..2d3f65ba57 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthrough.java @@ -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 index 0000000000..f78fff3553 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePassthroughSession.java @@ -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/helpers/TapoCipher.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePasstroughCipher.java similarity index 62% rename from bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java rename to bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/aes/SecurePasstroughCipher.java index ca0d409935..0e6f54fdc2 100644 --- 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/api/protocol/aes/SecurePasstroughCipher.java @@ -10,7 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tapocontrol.internal.helpers; +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; @@ -21,6 +24,8 @@ 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; @@ -31,8 +36,8 @@ import org.slf4j.LoggerFactory; * @author Christian Wild - Initial Initial contribution */ @NonNullByDefault -public class TapoCipher { - private final Logger logger = LoggerFactory.getLogger(TapoCipher.class); +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"; @@ -44,37 +49,35 @@ public class TapoCipher { private Cipher encodeCipher; @NonNullByDefault({}) private Cipher decodeCipher; - @NonNullByDefault({}) - private MimeEncode mimeEncode; /** * CREATE NEW EMPTY CIPHER */ - public TapoCipher() { + public SecurePasstroughCipher() { } /** * CREATE NEW CIPHER WITH KEY AND CREDENTIALS * - * @param handshakeKey Key from Handshake-Request - * @param credentials TapoCredentials + * @param handshakeKey key from Handshake-Request + * @param keyPair keyPair + * @throws TapoErrorHandler */ - public TapoCipher(String handshakeKey, TapoCredentials credentials) { - setKey(handshakeKey, credentials); + public SecurePasstroughCipher(String handshakeKey, TapoKeyPair keyPair) throws TapoErrorHandler { + setKey(handshakeKey, keyPair); } /** * SET NEW KEY AND CREDENTIALS * - * @param handshakeKey - * @param credentials + * @param handshakeKey key from Handshake-Request + * @param keyPair keyPair */ - public void setKey(String handshakeKey, TapoCredentials credentials) { - logger.trace("Init TapoCipher with key: {} ", handshakeKey); - MimeEncode mimeEncode = new MimeEncode(); + public void setKey(String handshakeKey, TapoKeyPair keyPair) throws TapoErrorHandler { + logger.trace("Init passtroughCipher with key: {} ", handshakeKey); try { - byte[] decode = mimeEncode.decode(handshakeKey.getBytes(HANDSHAKE_CHARSET)); - byte[] decode2 = mimeEncode.decode(credentials.getPrivateKeyBytes()); + 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)); @@ -85,8 +88,9 @@ public class TapoCipher { 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()); + } catch (Exception e) { + logger.warn("handshake Failed: {}", e.getMessage()); + throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, e.getMessage()); } } @@ -99,17 +103,16 @@ public class TapoCipher { */ 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); + 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()); - this.encodeCipher = null; - this.decodeCipher = null; + encodeCipher = null; + decodeCipher = null; } } @@ -122,8 +125,8 @@ public class TapoCipher { */ public String encode(String str) throws Exception { byte[] doFinal; - doFinal = this.encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET)); - String encrypted = mimeEncode.encodeToString(doFinal); + doFinal = encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET)); + String encrypted = getMimeEncoder().encodeToString(doFinal); return encrypted.replace("\r\n", ""); } @@ -135,9 +138,9 @@ public class TapoCipher { * @throws Exception */ public String decode(String str) throws Exception { - byte[] data = mimeEncode.decode(str.getBytes(CIPHER_CHARSET)); + byte[] data = getMimeDecoder().decode(str.getBytes(CIPHER_CHARSET)); byte[] doFinal; - doFinal = this.decodeCipher.doFinal(data); + 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 index 0000000000..44e981a1fc --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapCipher.java @@ -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 index 0000000000..4e3c59b720 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapProtocol.java @@ -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 index 0000000000..dfaba69914 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/klap/KlapSession.java @@ -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 index 0000000000..77b05f881e --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/protocol/passthrough/PassthroughProtocol.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java index 79d098f873..6ed25d5c71 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java @@ -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 index 0000000000..f72b9021c2 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoComConstants.java @@ -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 DEVICE_CMDLIST_QUERY = Set.of(DEVICE_CMD_GETINFO, DEVICE_CMD_GETENERGY, + DEVICE_CMD_GETCHILDDEVICELIST); + public static final Set DEVICE_CMDLIST_SET = Set.of(DEVICE_CMD_SETINFO, DEVICE_CMD_SET_DYNAIMCLIGHT_FX, + DEVICE_CMD_CONTROL_CHILD); + + public static final int LOGIN_RETRIES = 1; +} diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorCode.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorCode.java index af792871d5..5c7e411fa1 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorCode.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorCode.java @@ -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; diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoThingConstants.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoThingConstants.java index dbc1dac047..0286783a64 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoThingConstants.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoThingConstants.java @@ -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 SUPPORTED_BRIDGE_UIDS = Set.of(BRIDGE_THING_TYPE); - public static final Set 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 SUPPORTED_HUB_UIDS = Set.of(H100_THING_TYPE); + public static final Set SUPPORTED_SOCKET_UIDS = Set.of(P100_THING_TYPE, P105_THING_TYPE, + P110_THING_TYPE, P115_THING_TYPE); + public static final Set SUPPORTED_SOCKET_STRIP_UIDS = Set.of(P300_THING_TYPE); public static final Set SUPPORTED_WHITE_BULB_UIDS = Set.of(L510_THING_TYPE, L610_THING_TYPE); public static final Set SUPPORTED_COLOR_BULB_UIDS = Set.of(L530_THING_TYPE, L630_THING_TYPE); public static final Set SUPPORTED_LIGHT_STRIP_UIDS = Set.of(L900_THING_TYPE, L920_THING_TYPE, L930_THING_TYPE); - public static final Set 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 SUPPORTED_HUB_CHILD_TYPES_UIDS = Set.of(T110_THING_TYPE, T310_THING_TYPE, + T315_THING_TYPE); + public static final Set SUPPORTED_SMART_CONTACTS = Set.of(T110_THING_TYPE); + public static final Set SUPPORTED_MOTION_SENSORS = Set.of(); + public static final Set SUPPORTED_WHEATHER_SENSORS = Set.of(T310_THING_TYPE, T315_THING_TYPE); + + /*** SET OF ALL SUPPORTED THINGS ***/ + public static final Set 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 SUPPORTED_ENERGY_DATA_UIDS = Set.of(P110_THING_TYPE, P115_THING_TYPE); - /*** THINGS WITH CHILDS DATA ***/ - public static final Set SUPPORTED_CHILDS_DATA_UIDS = Set.of(P300_THING_TYPE); - /*** THINGS WITH CHANNEL GROUPS ***/ - public static final Set 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 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/TapoLightStrip.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java deleted file mode 100644 index d38df6c685..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java +++ /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 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 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 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 index 9db4089199..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartBulb.java +++ /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 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 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 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 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 index 410c02f865..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java +++ /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 index 547d6ac34b..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java +++ /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 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 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 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/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeConfiguration.java similarity index 81% rename from bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java rename to bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeConfiguration.java index f787f5cb49..5075e689c8 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeConfiguration.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tapocontrol.internal.structures; +package org.openhab.binding.tapocontrol.internal.devices.bridge; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -27,6 +27,8 @@ public final class TapoBridgeConfiguration { 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 */ @@ -35,8 +37,10 @@ public final class TapoBridgeConfiguration { /* 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/device/TapoBridgeHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeHandler.java similarity index 64% rename from bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java rename to bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/TapoBridgeHandler.java index 5624097c9e..524b8e0f58 100644 --- 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/devices/bridge/TapoBridgeHandler.java @@ -10,7 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tapocontrol.internal.device; +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; @@ -20,11 +22,10 @@ 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.discovery.TapoDiscoveryService; 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; @@ -37,8 +38,6 @@ 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. @@ -53,7 +52,6 @@ public class TapoBridgeHandler extends BaseBridgeHandler { 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; @@ -63,9 +61,9 @@ public class TapoBridgeHandler extends BaseBridgeHandler { 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(); + cloudConnector = new TapoCloudConnector(this); + credentials = new TapoCredentials(); + uid = thing.getUID().toString(); this.httpClient = httpClient; } @@ -80,8 +78,8 @@ public class TapoBridgeHandler extends BaseBridgeHandler { * set credentials and login cloud */ public void initialize() { - this.config = getConfigAs(TapoBridgeConfiguration.class); - this.credentials = new TapoCredentials(config.username, config.password); + config = getConfigAs(TapoBridgeConfiguration.class); + credentials = new TapoCredentials(config.username, config.password); activateBridge(); } @@ -105,7 +103,6 @@ public class TapoBridgeHandler extends BaseBridgeHandler { public void dispose() { stopScheduler(this.startupJob); stopScheduler(this.pollingJob); - stopScheduler(this.discoveryJob); super.dispose(); } @@ -138,7 +135,7 @@ public class TapoBridgeHandler extends BaseBridgeHandler { private void delayedStartUp() { loginCloud(); startCloudScheduler(); - startDiscoveryScheduler(); + discoveryService.startBackgroundDiscovery(); } /** @@ -158,26 +155,10 @@ public class TapoBridgeHandler extends BaseBridgeHandler { } } - /** - * 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 + * @param scheduler ScheduledFeature which should be stopped */ protected void stopScheduler(@Nullable ScheduledFuture scheduler) { if (scheduler != null) { @@ -191,13 +172,14 @@ public class TapoBridgeHandler extends BaseBridgeHandler { * ERROR HANDLER * ************************************/ + /** * return device Error * * @return */ - public TapoErrorHandler getError() { - return this.bridgeError; + public TapoErrorHandler getErrorHandler() { + return bridgeError; } /** @@ -206,7 +188,8 @@ public class TapoBridgeHandler extends BaseBridgeHandler { * @param tapoError TapoErrorHandler-Object */ public void setError(TapoErrorHandler tapoError) { - this.bridgeError.set(tapoError); + bridgeError.set(tapoError); + handleConnectionState(); } /*********************************** @@ -222,63 +205,39 @@ public class TapoBridgeHandler extends BaseBridgeHandler { */ 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()); + if (credentials.areSet()) { + try { + cloudConnector.login(credentials); + } catch (Exception e) { + logger.trace("({}) login to cloud failed", this.uid); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set"); + bridgeError.raiseError(ERR_BINDING_CREDENTIALS, "credentials not set"); } - return false; + handleConnectionState(); + return cloudConnector.isLoggedIn(); } - /*********************************** - * - * DEVICE DISCOVERY - * - ************************************/ - /** - * START DEVICE DISCOVERY + * Handle Connection state */ - 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(); + 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 { - 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(); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE); } - return deviceList; } /*********************************** @@ -288,11 +247,19 @@ public class TapoBridgeHandler extends BaseBridgeHandler { ************************************/ public TapoCredentials getCredentials() { - return this.credentials; + return credentials; } public HttpClient getHttpClient() { - return this.httpClient; + return httpClient; + } + + public TapoCloudConnector getCloudConnector() { + return cloudConnector; + } + + public TapoDiscoveryService getDiscoveryService() { + return discoveryService; } public ThingUID getUID() { @@ -300,6 +267,6 @@ public class TapoBridgeHandler extends BaseBridgeHandler { } public TapoBridgeConfiguration getBridgeConfig() { - return this.config; + 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 index 0000000000..c9d67605a8 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginData.java @@ -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 index 0000000000..0180d6a8ec --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/bridge/dto/TapoCloudLoginResult.java @@ -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 index 0000000000..9bc98ab441 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoBaseDeviceData.java @@ -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 index 0000000000..491c78bbd1 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildDeviceData.java @@ -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/structures/TapoChildData.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildList.java similarity index 59% rename from bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoChildData.java rename to bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoChildList.java index 3cd05f3beb..2470d245ed 100644 --- 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/devices/dto/TapoChildList.java @@ -10,22 +10,38 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tapocontrol.internal.structures; +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 TapoChildData { +public class TapoChildList { + @Expose + @SerializedName("start_index") private int startIndex = 0; + + @Expose private int sum = 0; - private List childDeviceList = List.of(); + + @Expose + @SerializedName("child_device_list") + private List childDeviceList = List.of(); + + /*********************************** + * + * GET VALUES + * + ************************************/ public int getStartIndex() { return startIndex; @@ -35,7 +51,7 @@ public class TapoChildData { return sum; } - public List getChildDeviceList() { + public List 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 index 0000000000..7d2a9dc1e0 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoEnergyData.java @@ -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 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 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 index 0000000000..768734cce3 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightDynamicFx.java @@ -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 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 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>() { + }.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 index 0000000000..184c9d45cd --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/dto/TapoLightEffect.java @@ -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 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 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 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 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 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 index 0000000000..6ec04a8374 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/TapoChildDeviceHandler.java @@ -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 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 true if changed, false 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 index 0000000000..60c15bfa75 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/smartcontact/TapoSmartContactHandler.java @@ -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 index 0000000000..2616b84eaa --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/rf/wheatersensor/TapoWheaterSensorHandler.java @@ -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/device/TapoDevice.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoBaseDeviceHandler.java similarity index 57% rename from bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java rename to bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoBaseDeviceHandler.java index 2158f78d4d..b44d2b08a4 100644 --- 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/devices/wifi/TapoBaseDeviceHandler.java @@ -10,14 +10,18 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tapocontrol.internal.device; +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.TapoUtils.*; +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; @@ -26,14 +30,14 @@ 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.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; @@ -41,7 +45,6 @@ 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; @@ -51,12 +54,13 @@ import org.slf4j.LoggerFactory; * @author Christian Wild - Initial contribution */ @NonNullByDefault -public abstract class TapoDevice extends BaseThingHandler { - private final Logger logger = LoggerFactory.getLogger(TapoDevice.class); +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 config = new TapoDeviceConfiguration(); - protected TapoDeviceInfo deviceInfo; + 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; @@ -67,16 +71,13 @@ public abstract class TapoDevice extends BaseThingHandler { * * @param thing Thing object representing device */ - protected TapoDevice(Thing thing) { + protected TapoBaseDeviceHandler(Thing thing) { super(thing); - this.deviceInfo = new TapoDeviceInfo(); this.uid = getThing().getUID().getAsString(); } /*********************************** - * * INIT AND SETTINGS - * ************************************/ /** @@ -85,23 +86,30 @@ public abstract class TapoDevice extends BaseThingHandler { @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); - } + 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("({}) configuration error : {}", uid, e.getMessage()); + logger.debug("({}) error initializing device : {}", uid, e.getMessage()); } - TapoErrorHandler configError = checkSettings(); - if (!configError.hasError()) { - activateDevice(); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.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); + } } } @@ -123,7 +131,7 @@ public abstract class TapoDevice extends BaseThingHandler { /** * ACTIVATE DEVICE */ - private void activateDevice() { + protected void activateDevice() { // set the thing status to UNKNOWN temporarily and let the background task decide for the real status. updateStatus(ThingStatus.UNKNOWN); @@ -132,29 +140,25 @@ public abstract class TapoDevice extends BaseThingHandler { } /** - * CHECK SETTINGS - * - * @return TapoErrorHandler with configuration-errors + * Check if bridge is set */ - protected TapoErrorHandler checkSettings() { - TapoErrorHandler configErr = new TapoErrorHandler(); - + protected boolean checkBridge() throws 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; + throw new TapoErrorHandler(ERR_CONFIG_NO_BRIDGE); } /* check credentials */ if (!bridge.getCredentials().areSet()) { - configErr.raiseError(ERR_CONFIG_CREDENTIALS); - return configErr; + throw new TapoErrorHandler(ERR_CONFIG_CREDENTIALS); } - return configErr; + return true; + } + + /** + * Check if Bridge is set and deviceConfiguration is set + */ + protected boolean checkRequirements() throws TapoErrorHandler { + return (checkBridge() && deviceConfig.checkConfig()); } /** @@ -171,9 +175,7 @@ public abstract class TapoDevice extends BaseThingHandler { } /*********************************** - * * SCHEDULER - * ************************************/ /** * delayed OneTime StartupJob @@ -187,7 +189,7 @@ public abstract class TapoDevice extends BaseThingHandler { * Start scheduler */ protected void startPollingScheduler() { - int pollingInterval = this.config.pollingInterval; + int pollingInterval = deviceConfig.pollingInterval; TimeUnit timeUnit = TimeUnit.SECONDS; if (pollingInterval > 0) { @@ -198,7 +200,7 @@ public abstract class TapoDevice extends BaseThingHandler { this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval, pollingInterval, timeUnit); } else { - logger.debug("({}) scheduler disabled with config '0'", uid); + logger.debug("({}) scheduler disabled with deviceConfig '0'", uid); stopScheduler(this.pollingJob); } } @@ -206,12 +208,11 @@ public abstract class TapoDevice extends BaseThingHandler { /** * Stop scheduler * - * @param scheduler {@code ScheduledFeature} which schould be stopped + * @param scheduler ScheduledFeature which should be stopped */ protected void stopScheduler(@Nullable ScheduledFuture scheduler) { if (scheduler != null) { scheduler.cancel(true); - scheduler = null; } } @@ -220,25 +221,21 @@ public abstract class TapoDevice extends BaseThingHandler { */ protected void pollingSchedulerAction() { logger.trace("({}) schedulerAction", uid); - queryDeviceInfo(); + queryDeviceData(); } /*********************************** - * * ERROR HANDLER - * ************************************/ /** * return device Error - * - * @return */ public TapoErrorHandler getErrorHandler() { - return this.deviceError; + return deviceError; } public TapoErrorCode getError() { - return this.deviceError.getError(); + return deviceError.getError(); } /** @@ -247,14 +244,12 @@ public abstract class TapoDevice extends BaseThingHandler { * @param tapoError TapoErrorHandler-Object */ public void setError(TapoErrorHandler tapoError) { - this.deviceError.set(tapoError); + deviceError.set(tapoError); handleConnectionState(); } /*********************************** - * * THING - * ************************************/ /*** @@ -265,6 +260,7 @@ public abstract class TapoDevice extends BaseThingHandler { */ protected Boolean isThingModel(String model) { try { + model = getDeviceModel(model); ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model); ThingTypeUID expectedType = getThing().getThingTypeUID(); return expectedType.equals(foundType); @@ -278,14 +274,14 @@ public abstract class TapoDevice extends BaseThingHandler { * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE * Compare MAC-Adress * - * @param deviceInfo + * @param baseDeviceData basebaseDeviceData * @return true if is the expected device */ - protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) { + protected boolean isExpectedThing(TapoBaseDeviceData baseDeviceData) { try { String expectedThingUID = getThing().getProperties().get(DEVICE_REPRESENTATION_PROPERTY); - String foundThingUID = deviceInfo.getRepresentationProperty(); - String foundModel = deviceInfo.getModel(); + String foundThingUID = baseDeviceData.getRepresentationProperty(); + String foundModel = baseDeviceData.getModel(); if (expectedThingUID == null || expectedThingUID.isBlank()) { return isThingModel(foundModel); } @@ -306,120 +302,101 @@ public abstract class TapoDevice extends BaseThingHandler { return getThing().getUID(); } + /** + * Return ipAdress + */ + public String getIpAddress() { + return deviceConfig.ipAddress; + } + + /* + * return device configuration + */ + public TapoDeviceConfiguration getDeviceConfig() { + return deviceConfig; + } + /*********************************** - * * DEVICE PROPERTIES - * ************************************/ /** - * query device Properties + * query default device properties + * query baseDeviceData, energyData (if available for device) and childData (if available for device). */ - public void queryDeviceInfo() { - queryDeviceInfo(false); + public void queryDeviceData() { + queryDeviceData(false); } /** - * query device Properties - * + * query default device properties + * query baseDeviceData, energyData (if available for device) + * * @param ignoreGap ignore gap to last query. query anyway (force) */ - public void queryDeviceInfo(boolean ignoreGap) { + public void queryDeviceData(boolean ignoreGap) { deviceError.reset(); - if (connector.loggedIn()) { - connector.queryInfo(ignoreGap); - // query energy usage + if (isLoggedIn(LOGIN_RETRIES)) { if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) { - connector.getEnergyUsage(); - } - // query childs data - if (SUPPORTED_CHILDS_DATA_UIDS.contains(getThing().getThingTypeUID())) { - connector.queryChildDevices(); + List 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); } - } else { - logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid); - connect(); } } /** - * SET DEVICE INFOs to device - * - * @param deviceInfo + * 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 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"); + 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(); } /** - * 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 + * handle baseDeviceData */ - public void setChildData(TapoChildData hostData) { - hostData.getChildDeviceList().forEach(child -> { - publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT + Integer.toString(child.getPosition())), - getOnOffType(child.getDeviceOn())); - }); + 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 responseBody + * + * @param fullResponse complete TapoResponse */ - 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 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); + public void responsePasstrough(TapoResponse fullResponse) { } /*********************************** @@ -432,14 +409,14 @@ public abstract class TapoDevice extends BaseThingHandler { * Connect (login) to device * */ - public Boolean connect() { + public boolean connect() { deviceError.reset(); - Boolean loginSuccess = false; + boolean loginSuccess = false; try { loginSuccess = connector.login(); if (loginSuccess) { - queryDeviceInfo(true); + queryDeviceData(true); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage()); } @@ -456,6 +433,23 @@ public abstract class TapoDevice extends BaseThingHandler { 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 */ @@ -485,18 +479,53 @@ public abstract class TapoDevice extends BaseThingHandler { } } + /*********************************** + * CHANNELS + ************************************/ + /** - * Return IP-Address of device + * 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 */ - public String getIpAddress() { - return this.config.ipAddress; + protected void updateDeviceProperties(TapoBaseDeviceData baseDeviceData) { + /* device properties */ + Map 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 baseDeviceData) { } - /*********************************** - * - * CHANNELS - * - ************************************/ /** * Get ChannelID including group * @@ -511,19 +540,4 @@ public abstract class TapoDevice extends BaseThingHandler { } 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/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 index 0000000000..bb96bd3405 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoDeviceConfiguration.java @@ -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 index 0000000000..262f2f3a25 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/TapoUniversalDeviceHandler.java @@ -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 index 0000000000..2b611e0920 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbData.java @@ -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 index 0000000000..9f1194ec16 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbHandler.java @@ -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 index 0000000000..498417eb76 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbLastStates.java @@ -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 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/structures/TapoDeviceConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbModeEnum.java similarity index 50% rename from bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java rename to bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/bulb/TapoBulbModeEnum.java index df9ce096ab..0090a2c31f 100644 --- 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/devices/wifi/bulb/TapoBulbModeEnum.java @@ -10,23 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tapocontrol.internal.structures; +package org.openhab.binding.tapocontrol.internal.devices.wifi.bulb; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters. + * Tapo-Bulb-Mode Enum * * @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; +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 index 0000000000..8f089bfe10 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubData.java @@ -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 index 0000000000..7575d58f60 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/hub/TapoHubHandler.java @@ -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 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> 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 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 getChildDevices() { + return tapoChildsList.getChildDeviceList(); + } + + public TapoChildDeviceData getChild(String deviceSerial) { + List 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 index 0000000000..313bcf3a66 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripData.java @@ -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 index 0000000000..4e9c721c2c --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/lightstrip/TapoLightStripHandler.java @@ -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 index 0000000000..600235e57f --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketData.java @@ -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 index 0000000000..7c641dfd80 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketHandler.java @@ -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 index 0000000000..83a0860c5e --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/devices/wifi/socket/TapoSocketStripHandler.java @@ -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 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 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 index 0000000000..b641fb6ec8 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoChildDiscoveryService.java @@ -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 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 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 index 0000000000..f262836319 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoDiscoveryService.java @@ -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 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 index 0000000000..31adcf4ae8 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/TapoUdpDiscovery.java @@ -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 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 listAllBroadcastAddresses() throws TapoErrorHandler { + try { + List broadcastList = new ArrayList<>(); + Enumeration 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 index 0000000000..c48944fde5 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResult.java @@ -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 index 0000000000..21b42c81fc --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/discovery/dto/TapoDiscoveryResultList.java @@ -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 { + + @Expose + private List 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 deviceList() { + return Objects.requireNonNullElse(deviceList, List.of()); + } + + @Override + public Iterator 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 index 0000000000..bbad8caf9a --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoBaseRequestInterface.java @@ -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 index 0000000000..8704ce7c47 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoChildRequest.java @@ -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 index 0000000000..1c08d3faa7 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoMultipleRequest.java @@ -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 requests) { + } + + public TapoMultipleRequest(TapoRequest... requests) { + this(DEVICE_CMD_MULTIPLE_REQ, new SubRequest(Arrays.asList(requests)), System.currentTimeMillis()); + } + + public TapoMultipleRequest(List 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 index 0000000000..5751067283 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoRequest.java @@ -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 index 0000000000..cfdebc7dfe --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/dto/TapoResponse.java @@ -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 responses() { + JsonArray responses = result.getAsJsonArray(MULTI_RESPONSE_KEY); + Type repsonseListType = new TypeToken>() { + }.getType(); + return Objects.requireNonNullElse(GSON.fromJson(responses.toString(), repsonseListType), + new ArrayList()); + } + + @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 index 056839b63b..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java +++ /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 index fcf05993a4..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/PayloadBuilder.java +++ /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/TapoCredentials.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCredentials.java index 2ccf434f62..f99f054947 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCredentials.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCredentials.java @@ -12,17 +12,7 @@ */ 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 index 0000000000..8776d9f899 --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoEncoder.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoErrorHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoErrorHandler.java index bfec0f5e52..196b9d5bb4 100644 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoErrorHandler.java +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoErrorHandler.java @@ -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 index 0000000000..164454342a --- /dev/null +++ b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoKeyPair.java @@ -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 index 21a66ae8bd..0000000000 --- a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoUtils.java +++ /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 Type of value - * @param value value - * @param defaultValue defaut value - * @return - */ - public static 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