]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Tapocontrol] Binding to control Tapo (by TP-Link) Devices (#11111)
authorChristian Wild <40909464+wildcs@users.noreply.github.com>
Sun, 28 Nov 2021 14:29:21 +0000 (15:29 +0100)
committerGitHub <noreply@github.com>
Sun, 28 Nov 2021 14:29:21 +0000 (15:29 +0100)
* [tapocontrol] New Source Upload

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] Delete bundles/org.openhab.binding.tapocontrol directory

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] Snapshot 3.2

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] Update CODEOWNERS

Fixed bindingname

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] Update README.md

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] new "Bridge-Version"

Credentials (TapoCloud) where now set in a bridge device.
Things now had to be attached to a bridge.

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] fixed device discovery bug

fixed device discovery bug
added bridge to thing-types.xml

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] Update bundles/org.openhab.binding.tapocontrol/README.md

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] code cleanup and optimization

- general code cleanup and optimization
- limited max connections and queued requests to 10 per destination
- device error handling revised
- review remarks of pull request processed

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] solved review requests

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] LightStrip L900 basicly supported

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] fixed review requests

Signed-off-by: Christian Wild <christian@wild-bw.de>
* [tapocontrol] fixed compiler warnings

Signed-off-by: Christian Wild <christian@wild-bw.de>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
48 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.tapocontrol/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/README.md [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoControlHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/TapoDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoCloudConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/api/TapoDeviceHttpApi.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoThingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartBulb.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/PayloadBuilder.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCredentials.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoErrorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L510_Series.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L530_Series.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L900.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/P100.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/P105.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channels.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/api/TapoUDP.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/thing/testdevice.xml [new file with mode: 0644]
bundles/pom.xml

index beaf69a9962c31f329e9bcb051aa5a3608e327cb..44df2624eaa7a6c73f31593966f1164ee7555de6 100644 (file)
 /bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis
 /bundles/org.openhab.binding.tado/ @dfrommi
 /bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag
+/bundles/org.openhab.binding.tapocontrol/ @wildcs
 /bundles/org.openhab.binding.telegram/ @ZzetT
 /bundles/org.openhab.binding.teleinfo/ @Nokyyz @olivierkeke
 /bundles/org.openhab.binding.tellstick/ @openhab/add-ons-maintainers
index 0e49501a5ad761e9ef40ed846aea6f3d10bff15a..d672adddd5321f59f92b27e7b4d4f7519fce9cbd 100644 (file)
       <artifactId>org.openhab.binding.tankerkoenig</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.tapocontrol</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.telegram</artifactId>
diff --git a/bundles/org.openhab.binding.tapocontrol/NOTICE b/bundles/org.openhab.binding.tapocontrol/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.tapocontrol/README.md b/bundles/org.openhab.binding.tapocontrol/README.md
new file mode 100644 (file)
index 0000000..77074f4
--- /dev/null
@@ -0,0 +1,114 @@
+# TapoControl Binding
+
+This binding adds support to control Tapo (Copyright © TP-Link Corporation Limited) Smart Home Devices from your local openHAB system.
+
+## Supported Things
+
+The following Tapo-Devices are supported
+
+### P100/P105 SmartPlug (WiFi)
+
+* Power On/Off
+* Wi-Fi signal (SignalStrength)
+* On-Time (Time in seconds device is switched on)
+
+### L510_Series dimmable SmartBulb (WiFi)
+
+* Light On/Off
+* Brightnes (Dimmer)  0-100 %
+* ColorTemperature (Number) 2500-6500 K
+* Wi-Fi signal (SignalStrength)
+* On-Time (Time in seconds device is switched on)
+
+### L530_Series MultiColor SmartBulb (WiFi)
+
+* Light On/Off
+* Brightnes (Dimmer)  0-100 %
+* ColorTemperature (Number) 2500-6500 K
+* Color (Color)
+* Wi-Fi signal (SignalStrength)
+* On-Time (Time in seconds device is switched on)
+
+### L900 MultiColor LightStrip (WiFi)
+
+* Light On/Off
+* Brightnes (Dimmer)  0-100 %
+* ColorTemperature (Number) 2500-6500 K
+* Color (Color)
+* Wi-Fi signal (SignalStrength)
+* On-Time (Time in seconds device is switched on)
+
+
+## Prerequisites
+
+Before using Smart Plugs with openHAB the devices must be connected to the Wi-Fi network.
+This can be done using the Tapo provided mobile app.
+You need to setup a bridge (Cloud-Login) to commiunicate with your devices.
+
+## 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
+
+## Bridge Configuration
+
+The bridge needs to be configured with by `username` and `password` (Tapo-Cloud login) .
+This is used for device discovery and to create a handshake (cookie) to act with your devices over the local network.
+
+The thing has the following configuration parameters:
+
+| Parameter          | Description                                                          |
+|--------------------|----------------------------------------------------------------------|
+| username           | Username (eMail) of your Tapo-Cloud                                  |
+| password           | Password of your Tapo-Cloud                                          |
+
+## Thing Configuration
+
+The thing needs to be configured with `ipAddress`.
+
+The thing has the following configuration parameters:
+
+| Parameter          | Description                                                          |
+|--------------------|----------------------------------------------------------------------|
+| ipAddress          | IP Address of the device.                                            |
+| pollingInterval    | Refresh interval in seconds. Optional. The default is 30 seconds     |
+
+
+## Channels
+
+All devices support some of the following channels:
+
+| group     | channel          |type                    | description                  | things supporting this channel  |
+|-----------|----------------- |------------------------|------------------------------|---------------------------------|
+| actuator  | output           | Switch                 | Power device on or off       | P100, P105,L510, L530, L900     |
+|           | brightness       | Dimmer                 | Brightness 0-100%            | L510, L530, L900                |
+|           | colorTemperature | Number                 | White-Color-Temp 2500-6500K  | L510, L530, L900                |
+|           | color            | Color                  | Color                        | L530, L900                      |
+| device    | wifiSignal       | system.signal-strength | WiFi-quality-level           | P100, P105, L510, L530, L900    |
+|           | onTime           | Number:Time            | seconds output is on         | P100, P105, L510, L530, L900    |
+
+
+## Channel Refresh
+
+When the thing receives a `RefreshType` command the thing will send a new refreshRequest over http.
+To minimize network traffic the default refresh-rate is set to 30 seconds. This can be reduced down to 10 seconds in advanced settings of the device. If any command was sent to a channel, it will do an immediately refresh of the whole device.
+
+
+## Full Example
+
+### tapocontrol.things:
+
+```
+tapocontrol:bridge:myTapoBridge                     "Cloud-Login"               [ username="you@yourpovider.com", password="verysecret" ]
+tapocontrol:P100:myTapoBridge:mySocket              "My-Socket"                 [ ipAddress="192.168.178.150", pollingInterval=30 ]
+tapocontrol:L510_Series:myTapoBridge:whiteBulb      "white-light"               [ ipAddress="192.168.178.151", pollingInterval=30 ]
+tapocontrol:L530_Series:myTapoBridge:colorBulb      "color-light"               [ ipAddress="192.168.178.152", pollingInterval=30 ]
+tapocontrol:L900:myTapoBridge:myLightStrip          "light-strip"               [ ipAddress="192.168.178.153", pollingInterval=30 ]
+``` 
+
+### tapocontrol.items:
+
+```
+Switch       TAPO_SOCKET      "socket"                { channel="tapocontrol:P100:myTapoBridge:mySocket:actuator#output" }
+``` 
diff --git a/bundles/org.openhab.binding.tapocontrol/pom.xml b/bundles/org.openhab.binding.tapocontrol/pom.xml
new file mode 100644 (file)
index 0000000..2e71e41
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"  standalone="no"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.tapocontrol</artifactId>
+  <name>openHAB Add-ons :: Bundles :: TapoControl Binding</name>
+</project>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/feature/feature.xml b/bundles/org.openhab.binding.tapocontrol/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..5cc25ab
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.tapocontrol-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-tapocontrol" description="TapoControl Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.tapocontrol/${project.version}</bundle>
+       </feature>
+</features>
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
new file mode 100644 (file)
index 0000000..e24a5d5
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+
+import java.util.HashSet;
+import java.util.Set;
+
+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.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoControlHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@Component(service = ThingHandlerFactory.class, configurationPid = "binding.tapocontrol")
+@NonNullByDefault
+public class TapoControlHandlerFactory extends BaseThingHandlerFactory {
+    private final Logger logger = LoggerFactory.getLogger(TapoControlHandlerFactory.class);
+    private final Set<TapoBridgeHandler> accountHandlers = new HashSet<>();
+    private final HttpClient httpClient;
+
+    @Activate
+    public TapoControlHandlerFactory() {
+        // create new httpClient
+        httpClient = new HttpClient(new SslContextFactory.Client());
+        httpClient.setFollowRedirects(false);
+        httpClient.setMaxConnectionsPerDestination(HTTP_MAX_CONNECTIONS);
+        httpClient.setMaxRequestsQueuedPerDestination(HTTP_MAX_QUEUED_REQUESTS);
+        try {
+            httpClient.start();
+        } catch (Exception e) {
+            logger.error("cannot start httpClient");
+        }
+    }
+
+    @Deactivate
+    @Override
+    protected void deactivate(ComponentContext componentContext) {
+        super.deactivate(componentContext);
+        try {
+            httpClient.stop();
+        } catch (Exception e) {
+            logger.debug("unable to stop httpClient");
+        }
+    }
+
+    /**
+     * Provides the supported thing types
+     */
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        if (thingTypeUID.equals(UNIVERSAL_THING_TYPE)) {
+            return true;
+        }
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    /**
+     * Create handler of things.
+     */
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (SUPPORTED_BRIDGE_UIDS.contains(thingTypeUID)) {
+            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_WHITE_BULB_UIDS.contains(thingTypeUID)) {
+            return new TapoSmartBulb(thing);
+        } else if (SUPPORTED_COLOR_BULB_UIDS.contains(thingTypeUID)) {
+            return new TapoSmartBulb(thing);
+        } else if (SUPPORTED_LIGHT_STRIP_UIDS.contains(thingTypeUID)) {
+            return new TapoLightStrip(thing);
+        } else if (thingTypeUID.equals(UNIVERSAL_THING_TYPE)) {
+            return new TapoUniversalDevice(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
new file mode 100644 (file)
index 0000000..12bd4ea
--- /dev/null
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO Smart Home thing discovery
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+    private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
+    protected @NonNullByDefault({}) TapoBridgeHandler bridge;
+
+    /***********************************
+     *
+     * INITIALIZATION
+     *
+     ************************************/
+
+    /**
+     * INIT CLASS
+     * 
+     * @param bridgeHandler
+     */
+    public TapoDiscoveryService() {
+        super(SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
+    }
+
+    /**
+     * deactivate
+     */
+    @Override
+    public void activate() {
+        TapoBridgeConfiguration config = bridge.getBridgeConfig();
+        if (config.cloudDiscoveryEnabled || config.udpDiscoveryEnabled) {
+            startBackgroundDiscovery();
+        }
+    }
+
+    /**
+     * deactivate
+     */
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        if (handler instanceof TapoBridgeHandler) {
+            TapoBridgeHandler tapoBridge = (TapoBridgeHandler) handler;
+            tapoBridge.setDiscoveryService(this);
+            this.bridge = tapoBridge;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return this.bridge;
+    }
+
+    /***********************************
+     *
+     * SCAN HANDLING
+     *
+     ************************************/
+
+    /**
+     * Start scan manually
+     */
+    @Override
+    public void startScan() {
+        removeOlderResults(getTimestampOfLastScan());
+        if (bridge != null) {
+            JsonArray jsonArray = bridge.getDeviceList();
+            handleCloudDevices(jsonArray);
+        }
+    }
+
+    /***********************************
+     *
+     * handle Results
+     *
+     ************************************/
+
+    /**
+     * CREATE DISCOVERY RESULT
+     * creates discoveryResult (Thing) from JsonObject got from Cloud
+     * 
+     * @param device JsonObject with device information
+     * @return DiscoveryResult-Object
+     */
+    public DiscoveryResult createResult(JsonObject device) {
+        TapoBridgeHandler tapoBridge = this.bridge;
+        String deviceModel = getDeviceModel(device);
+        String label = getDeviceLabel(device);
+        String deviceMAC = device.get(CLOUD_PROPERTY_MAC).getAsString();
+        ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+        /* create properties */
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
+        properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(deviceMAC, MAC_DIVISION_CHAR));
+        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.get(CLOUD_PROPERTY_FW).getAsString());
+        properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.get(CLOUD_PROPERTY_HW).getAsString());
+        properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
+        properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.get(CLOUD_PROPERTY_ID).getAsString());
+
+        logger.debug("device {} discovered", deviceModel);
+        if (tapoBridge != null) {
+            ThingUID bridgeUID = tapoBridge.getUID();
+            ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
+            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                    .withRepresentationProperty(DEVICE_REPRASENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
+                    .build();
+        } else {
+            ThingUID thingUID = new ThingUID(BINDING_ID, deviceMAC);
+            return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+                    .withRepresentationProperty(DEVICE_REPRASENTATION_PROPERTY).withLabel(label).build();
+        }
+    }
+
+    /**
+     * work with result from get devices from cloud devices
+     * 
+     * @param deviceList
+     */
+    protected void handleCloudDevices(JsonArray deviceList) {
+        try {
+            for (JsonElement deviceElement : deviceList) {
+                if (deviceElement.isJsonObject()) {
+                    JsonObject device = deviceElement.getAsJsonObject();
+                    String deviceModel = getDeviceModel(device);
+                    ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+                    /* create thing */
+                    if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
+                        DiscoveryResult discoveryResult = createResult(device);
+                        thingDiscovered(discoveryResult);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.debug("error handlling CloudDevices", e);
+        }
+    }
+
+    /**
+     * GET DEVICEMODEL
+     * 
+     * @param device JsonObject with deviceData
+     * @return String with DeviceModel
+     */
+    protected String getDeviceModel(JsonObject device) {
+        try {
+            String deviceModel = device.get(CLOUD_PROPERTY_MODEL).getAsString();
+            deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
+            deviceModel = deviceModel.replace("Tapo", "");
+            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
new file mode 100644 (file)
index 0000000..30e7bdc
--- /dev/null
@@ -0,0 +1,238 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
+
+import java.util.concurrent.TimeoutException;
+
+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.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 {
+    private final Logger logger = LoggerFactory.getLogger(TapoCloudConnector.class);
+    private final TapoBridgeHandler bridge;
+    private final Gson gson = new Gson();
+    private final HttpClient httpClient;
+
+    private String token = "";
+    private String url = TAPO_CLOUD_URL;
+    private String uid;
+
+    /**
+     * INIT CLASS
+     * 
+     */
+    public TapoCloudConnector(TapoBridgeHandler bridge, HttpClient httpClient) {
+        this.bridge = bridge;
+        this.httpClient = httpClient;
+        this.uid = bridge.getUID().getAsString();
+    }
+
+    /**
+     * handle error
+     * 
+     * @param tapoError TapoErrorHandler
+     */
+    protected void handleError(TapoErrorHandler tapoError) {
+        this.bridge.setError(tapoError);
+    }
+
+    /***********************************
+     *
+     * HTTP (Cloud)-Actions
+     *
+     ************************************/
+
+    /**
+     * LOGIN TO CLOUD (get Token)
+     * 
+     * @param username unencrypted username
+     * @param password unencrypted password
+     * @return true if login was successfull
+     */
+    public Boolean login(String username, String password) {
+        this.token = getToken(username, password, TAPO_TERMINAL_UUID);
+        this.url = TAPO_CLOUD_URL + "?token=" + token;
+        return !this.token.isBlank();
+    }
+
+    /**
+     * logout
+     */
+    public void logout() {
+        this.token = "";
+    }
+
+    /**
+     * GET TOKEN FROM TAPO-CLOUD
+     * 
+     * @param email
+     * @param password
+     * @param terminalUUID
+     * @return
+     */
+    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;
+    }
+
+    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_JSON_DECODE_FAIL));
+                logger.trace("unexpected json-response '{}'", rBody);
+            }
+        } else {
+            handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE, ERR_HTTP_RESPONSE_MSG));
+            logger.warn("invalid response while login");
+            token = "";
+        }
+        return token;
+    }
+
+    /**
+     * 
+     * @return JsonArray with deviceList
+     */
+    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);
+        }
+        return new JsonArray();
+    }
+
+    /**
+     * get DeviceList from Contenresponse
+     * 
+     * @param response
+     * @return
+     */
+    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();
+    }
+
+    /***********************************
+     *
+     * 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 sendCloudRequest(String url, String payload) {
+        Request httpRequest = httpClient.newRequest(url).method(HttpMethod.POST.toString());
+
+        /* set header */
+        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 {
+            ContentResponse httpResponse = httpRequest.send();
+            return httpResponse;
+        } 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_CONNECT_TIMEOUT, e.toString()));
+        } catch (Exception e) {
+            logger.debug("({}) sending request failed: {}", uid, e.toString());
+            handleError(new TapoErrorHandler(e));
+        }
+        return null;
+    }
+}
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
new file mode 100644 (file)
index 0000000..bb0f5d9
--- /dev/null
@@ -0,0 +1,384 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+
+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.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO Smart Home device connections.
+ * This class uses asynchronous HttpClient-Requests
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDeviceConnector extends TapoDeviceHttpApi {
+    private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
+    private final String uid;
+    private final TapoDevice device;
+    private TapoDeviceInfo deviceInfo;
+    private Gson gson;
+    private long lastQuery = 0L;
+    private long lastSent = 0L;
+    private long lastLogin = 0L;
+
+    /**
+     * INIT CLASS
+     *
+     * @param config TapoControlConfiguration class
+     */
+    public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
+        super(device, bridgeThingHandler);
+        this.device = device;
+        this.gson = new Gson();
+        this.deviceInfo = new TapoDeviceInfo();
+        this.uid = device.getThingUID().getAsString();
+    }
+
+    /***********************************
+     *
+     * LOGIN FUNCTIONS
+     *
+     ************************************/
+    /**
+     * login
+     *
+     * @return true if success
+     */
+    public boolean login() {
+        if (this.pingDevice()) {
+            logger.trace("({}) sending login to url '{}'", uid, deviceURL);
+
+            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);
+                }
+            } 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);
+            handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE, "no ping while login"));
+            return false;
+        }
+    }
+
+    /***********************************
+     *
+     * DEVICE ACTIONS
+     *
+     ************************************/
+
+    /**
+     * send custom command to device
+     * 
+     * @param plBuilder Payloadbuilder with unencrypted payload
+     */
+    public void sendCustomQuery(String queryMethod) {
+        /* create payload */
+        PayloadBuilder plBuilder = new PayloadBuilder();
+        plBuilder.method = queryMethod;
+        sendCustomPayload(plBuilder);
+    }
+
+    /**
+     * send custom command to device
+     * 
+     * @param plBuilder Payloadbuilder with unencrypted payload
+     */
+    public void sendCustomPayload(PayloadBuilder plBuilder) {
+        long now = System.currentTimeMillis();
+        if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
+            String payload = plBuilder.getPayload();
+            sendSecurePasstrhroug(payload, DEVICE_CMD_CUSTOM);
+        } else {
+            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+        }
+    }
+
+    /**
+     * send "set_device_info" command to device
+     *
+     * @param name Name of command to send
+     * @param value Value to send to control
+     */
+    public void sendDeviceCommand(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 = DEVICE_CMD_SETINFO;
+            plBuilder.addParameter(name, value);
+            String payload = plBuilder.getPayload();
+
+            sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
+        } else {
+            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+        }
+    }
+
+    /**
+     * send multiple "set_device_info" commands to device
+     *
+     * @param map HashMap<String, Object> (name, value of parameter)
+     */
+    public void sendDeviceCommands(HashMap<String, Object> map) {
+        long now = System.currentTimeMillis();
+        if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
+            this.lastSent = now;
+
+            /* create payload */
+            PayloadBuilder plBuilder = new PayloadBuilder();
+            plBuilder.method = DEVICE_CMD_SETINFO;
+            for (HashMap.Entry<String, Object> entry : map.entrySet()) {
+                plBuilder.addParameter(entry.getKey(), entry.getValue());
+            }
+            String payload = plBuilder.getPayload();
+
+            sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
+        } else {
+            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+        }
+    }
+
+    /**
+     * Query Info from Device adn refresh deviceInfo
+     */
+    public void queryInfo() {
+        queryInfo(false);
+    }
+
+    /**
+     * Query Info from Device adn refresh deviceInfo
+     * 
+     * @param ignoreGap ignore gap to last query. query anyway
+     */
+    public void queryInfo(boolean ignoreGap) {
+        logger.trace("({}) DeviceConnetor_queryInfo from '{}'", uid, 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 = DEVICE_CMD_GETINFO;
+            String payload = plBuilder.getPayload();
+
+            sendSecurePasstrhroug(payload, DEVICE_CMD_GETINFO);
+        } else {
+            logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastQuery);
+        }
+    }
+
+    /**
+     * SEND SECUREPASSTHROUGH
+     * encprypt payload and send to device
+     * 
+     * @param payload payload sent to device
+     * @param command command executed - this will handle result
+     */
+    protected void sendSecurePasstrhroug(String payload, String command) {
+        /* encrypt payload */
+        String encryptedPayload = encryptPayload(payload);
+
+        /* create secured payload */
+        PayloadBuilder plBuilder = new PayloadBuilder();
+        plBuilder.method = "securePassthrough";
+        plBuilder.addParameter("request", encryptedPayload);
+        String securePassthroughPayload = plBuilder.getPayload();
+
+        sendAsyncRequest(deviceURL, securePassthroughPayload, command);
+    }
+
+    /***********************************
+     *
+     * HANDLE RESPONSES
+     *
+     ************************************/
+
+    /**
+     * Handle SuccessResponse (setDeviceInfo)
+     * 
+     * @param responseBody String with responseBody from device
+     */
+    @Override
+    protected void handleSuccessResponse(String responseBody) {
+        JsonObject jsnResult = getJsonFromResponse(responseBody);
+        Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_JSON_DECODE_FAIL);
+        if (errorCode != 0) {
+            logger.debug("({}) set deviceInfo not succesfull: {}", uid, jsnResult);
+            this.device.handleConnectionState();
+        }
+        this.device.responsePasstrough(responseBody);
+    }
+
+    /**
+     * handle JsonResponse (getDeviceInfo)
+     * 
+     * @param responseBody String with responseBody from device
+     */
+    @Override
+    protected void handleDeviceResult(String responseBody) {
+        JsonObject jsnResult = getJsonFromResponse(responseBody);
+        if (jsnResult.has("device_id")) {
+            this.deviceInfo = new TapoDeviceInfo(jsnResult);
+            this.device.setDeviceInfo(deviceInfo);
+        } else {
+            this.deviceInfo = new TapoDeviceInfo();
+            this.device.handleConnectionState();
+        }
+        this.device.responsePasstrough(responseBody);
+    }
+
+    /**
+     * handle custom response
+     * 
+     * @param responseBody String with responseBody from device
+     */
+    @Override
+    protected void handleCustomResponse(String responseBody) {
+        this.device.responsePasstrough(responseBody);
+    }
+
+    /**
+     * handle error
+     * 
+     * @param te TapoErrorHandler
+     */
+    @Override
+    protected void handleError(TapoErrorHandler tapoError) {
+        this.device.setError(tapoError);
+    }
+
+    /**
+     * get Json from response
+     * 
+     * @param responseBody
+     * @return JsonObject with result
+     */
+    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 successfull */
+                    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_HTTP_RESPONSE));
+        return new JsonObject();
+    }
+
+    /***********************************
+     *
+     * GET RESULTS
+     *
+     ************************************/
+
+    /**
+     * Check if device is online
+     * 
+     * @return true if device is online
+     */
+    public Boolean isOnline() {
+        return isOnline(false);
+    }
+
+    /**
+     * Check if device is online
+     * 
+     * @param raiseError if true
+     * @return true if device is online
+     */
+    public Boolean isOnline(Boolean raiseError) {
+        if (pingDevice()) {
+            return true;
+        } else {
+            logger.trace("({})  device is offline (no ping)", uid);
+            if (raiseError) {
+                handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE));
+            }
+            logout();
+            return false;
+        }
+    }
+
+    /**
+     * IP-Adress
+     * 
+     * @return String ipAdress
+     */
+    public String getIP() {
+        return this.ipAddress;
+    }
+
+    /**
+     * PING IP Adress
+     * 
+     * @return true if ping successfull
+     */
+    public Boolean pingDevice() {
+        try {
+            InetAddress address = InetAddress.getByName(this.ipAddress);
+            return address.isReachable(TAPO_PING_TIMEOUT_MS);
+        } catch (Exception e) {
+            logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
+            return false;
+        }
+    }
+}
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
new file mode 100644 (file)
index 0000000..59bb9b5
--- /dev/null
@@ -0,0 +1,564 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
+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.Gson;
+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 {
+    private final Logger logger = LoggerFactory.getLogger(TapoDeviceHttpApi.class);
+    private final String uid;
+    private final TapoCipher tapoCipher;
+    private final TapoBridgeHandler bridge;
+    private Gson gson;
+    private String token = "";
+    private String cookie = "";
+    protected String deviceURL = "";
+    protected String ipAddress = "";
+
+    /**
+     * INIT CLASS
+     *
+     * @param config TapoControlConfiguration class
+     */
+    public TapoDeviceHttpApi(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
+        this.bridge = bridgeThingHandler;
+        this.tapoCipher = new TapoCipher();
+        this.gson = new Gson();
+        this.uid = device.getThingUID().getAsString();
+        String ipAddress = device.getIpAddress();
+        setDeviceURL(ipAddress);
+    }
+
+    /***********************************
+     *
+     * 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 custom response
+     * 
+     * @param responseBody String with responseBody from device
+     */
+    protected void handleCustomResponse(String responseBody) {
+    }
+
+    /**
+     * handle error
+     * 
+     * @param te TapoErrorHandler
+     */
+    protected void handleError(TapoErrorHandler tapoError) {
+    }
+
+    /***********************************
+     *
+     * 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.toString());
+            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_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_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_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_JSON_DECODE_FAIL);
+                if (errorCode == 0) {
+                    /* return result if set / else request was successfull */
+                    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_JSON_ENCODE_FAIL, "could not get token");
+            }
+        } else {
+            logger.debug("({}) invalid response while login", uid);
+            tapoError.raiseError(ERR_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 {
+            ContentResponse httpResponse = httpRequest.send();
+            return httpResponse;
+        } 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_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);
+        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_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_HTTP_RESPONSE, getContentAsString()));
+                    } else {
+                        /* request succesfull */
+                        String rBody = getContentAsString();
+                        rBody = decryptResponse(rBody);
+                        logger.trace("({}) requestCompleted '{}'", uid, rBody);
+                        /* handle result */
+                        switch (command) {
+                            case DEVICE_CMD_SETINFO:
+                                handleSuccessResponse(rBody);
+                                break;
+                            case DEVICE_CMD_GETINFO:
+                                handleDeviceResult(rBody);
+                                break;
+                            case DEVICE_CMD_CUSTOM:
+                                handleCustomResponse(rBody);
+                                break;
+                        }
+                    }
+                }
+            });
+        } 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_HTTP_RESPONSE;
+            }
+        } catch (Exception e) {
+            return ERR_HTTP_RESPONSE;
+        }
+    }
+
+    /**
+     * 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_JSON_DECODE_FAIL);
+            if (errorCode == 0) {
+                return 0;
+            } else {
+                logger.debug("({}) device returns errorcode '{}'", uid, errorCode);
+                handleError(new TapoErrorHandler(errorCode));
+                return errorCode;
+            }
+        } catch (Exception e) {
+            return ERR_HTTP_RESPONSE;
+        }
+    }
+
+    /**
+     * 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_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_LOGIN));
+            }
+            return false;
+        }
+    }
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    /**
+     * Set new ipAddress
+     * 
+     * @param 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 deviceURL
+     * @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/constants/TapoBindingSettings.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoBindingSettings.java
new file mode 100644 (file)
index 0000000..dd4ff8f
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.constants;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TapoBindingSettings} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBindingSettings {
+    public static final String BINDING_ID = "tapocontrol";
+
+    // List of all constant configurations
+    public static final String HTTP_HEADER_AUTH = "Authorization";
+    public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
+    public static final String HTTP_AUTH_TYPE_COOKIE = "cookie";
+    public static final String CONTENT_CHARSET = "UTF-8";
+    public static final String CONTENT_TYPE_JSON = "application/json";
+    public static final String TAPO_CLOUD_URL = "https://eu-wap.tplinkcloud.com";
+    public static final String TAPO_APP_TYPE = "Tapo_Ios";
+    public static final String TAPO_TERMINAL_UUID = "0A950402-7224-46EB-A450-7362CDB902A2";
+    public static final String TAPO_DEVICE_URL = "http://%s/app";
+    public static final Integer HTTP_MAX_CONNECTIONS = 10; // setMaxConnectionsPerDestination for HTTP-Client
+    public static final Integer HTTP_MAX_QUEUED_REQUESTS = 10; // setMaxRequestsQueuedPerDestination for HTTP-Client
+    public static final Integer TAPO_HTTP_TIMEOUT_MS = 5000; // http request 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_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)
+
+    // 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_CUSTOM = "custom_command";
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorConstants.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/constants/TapoErrorConstants.java
new file mode 100644 (file)
index 0000000..d2e8d88
--- /dev/null
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.constants;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TapoErrorConstants} class defines error-message constants
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoErrorConstants {
+    /****************************************
+     * LIST OF ERROR CODES
+     ****************************************/
+    // List of API-ErrorCodes
+    public static final Integer ERR_COMMON_FAILED = -1;
+    public static final Integer ERR_SESSION_TIMEOUT = 9999;
+    public static final Integer ERR_NULL_TRANSPORT = 1000;
+    public static final Integer ERR_REQUEST = 1002;
+    public static final Integer ERR_HAND_SHAKE_FAILED = 1100;
+    public static final Integer ERR_LOGIN_FAILED = 1111;
+    public static final Integer ERR_HTTP_TRANSPORT_FAILED = 1112;
+    public static final Integer ERR_MULTI_REQUEST_FAILED = 1200;
+    public static final Integer ERR_JSON_DECODE_FAIL = -1003;
+    public static final Integer ERR_JSON_ENCODE_FAIL = -1004;
+    public static final Integer ERR_AES_DECODE_FAIL = -1005;
+    public static final Integer ERR_REQUEST_LEN_ERROR = -1006;
+    public static final Integer ERR_CLOUD_FAILED = -1007;
+    public static final Integer ERR_PARAMS = -1008;
+    public static final Integer ERR_RSA_KEY_LENGTH = -1010;
+    public static final Integer ERR_SESSION_PARAM = -1101;
+    public static final Integer ERR_QUICK_SETUP = -1201;
+    public static final Integer ERR_DEVICE = -1301;
+    public static final Integer ERR_DEVICE_NEXT_EVENT = -1302;
+    public static final Integer ERR_FIRMWARE = -1401;
+    public static final Integer ERR_FIRMWARE_VER_ERROR = -1402;
+    public static final Integer ERR_LOGIN = -1501;
+    public static final Integer ERR_TIME = -1601;
+    public static final Integer ERR_TIME_SYS = -1602;
+    public static final Integer ERR_TIME_SAVE = -1603;
+    public static final Integer ERR_WIRELESS = -1701;
+    public static final Integer ERR_WIRELESS_UNSUPPORTED = -1702;
+    public static final Integer ERR_SCHEDULE = -1801;
+    public static final Integer ERR_SCHEDULE_FULL = -1802;
+    public static final Integer ERR_SCHEDULE_CONFLICT = -1803;
+    public static final Integer ERR_SCHEDULE_SAVE = -1804;
+    public static final Integer ERR_SCHEDULE_INDEX = -1805;
+    public static final Integer ERR_COUNTDOWN = -1901;
+    public static final Integer ERR_COUNTDOWN_CONFLICT = -1902;
+    public static final Integer ERR_COUNTDOWN_SAVE = -1903;
+    public static final Integer ERR_ANTITHEFT = -2001;
+    public static final Integer ERR_ANTITHEFT_CONFLICT = -2002;
+    public static final Integer ERR_ANTITHEFT_SAVE = -2003;
+    public static final Integer ERR_ACCOUNT = -2101;
+    public static final Integer ERR_STAT = -2201;
+    public static final Integer ERR_STAT_SAVE = -2202;
+    public static final Integer ERR_DST = -2301;
+    public static final Integer ERR_DST_SAVE = -2302;
+    // -20661
+
+    // List of Binding-ErrorCodes
+    public static final Integer ERR_HTTP_RESPONSE = 9001;
+    public static final Integer ERR_COOKIE = 9002;
+    public static final Integer ERR_CREDENTIALS = 9003;
+    public static final Integer ERR_DEVICE_OFFLINE = 9009;
+    public static final Integer ERR_CONNECT_TIMEOUT = 9010;
+
+    // List of Config-ErrorCodes
+    public static final Integer ERR_CONF_IP = 10001; // ip not set
+    public static final Integer ERR_CONF_CREDENTIALS = 10002; // credentials not set
+    public static final Integer ERR_NO_BRIDGE = 10003; // no bridge configured
+
+    /****************************************
+     * LIST OF ERROR MESSAGES
+     ****************************************/
+    // List of CLOUD-Error-Messages
+    public static final String ERR_COMMON_FAILED_MSG = ""; // -1;
+    public static final String ERR_SESSION_TIMEOUT_MSG = "Session Timeout"; // 9999;
+    public static final String ERR_NULL_TRANSPORT_MSG = ""; // 1000;
+    public static final String ERR_REQUEST_MSG = "Invalid request or command"; // 1002;
+    public static final String ERR_HAND_SHAKE_FAILED_MSG = "Can't create handshake"; // 1100;
+    public static final String ERR_LOGIN_FAILED_MSG = ""; // 1111;
+    public static final String ERR_HTTP_TRANSPORT_FAILED_MSG = ""; // 1112;
+    public static final String ERR_MULTI_REQUEST_FAILED_MSG = ""; // 1200;
+    public static final String ERR_JSON_DECODE_FAIL_MSG = "json decode failed"; // -1003;
+    public static final String ERR_JSON_ENCODE_FAIL_MSG = "json encode failed"; // -1004;
+    public static final String ERR_AES_DECODE_FAIL_MSG = ""; // -1005;
+    public static final String ERR_REQUEST_LEN_ERROR_MSG = ""; // -1006;
+    public static final String ERR_CLOUD_FAILED_MSG = ""; // -1007;
+    public static final String ERR_PARAMS_MSG = "received invalid parameter"; // -1008;
+    public static final String ERR_RSA_KEY_LENGTH_MSG = "Invalid Public Key Length"; // -1010;
+    public static final String ERR_SESSION_PARAM_MSG = ""; // -1101;
+    public static final String ERR_QUICK_SETUP_MSG = ""; // -1201;
+    public static final String ERR_DEVICE_MSG = ""; // -1301;
+    public static final String ERR_DEVICE_NEXT_EVENT_MSG = ""; // -1302;
+    public static final String ERR_FIRMWARE_MSG = ""; // -1401;
+    public static final String ERR_FIRMWARE_VER_ERROR_MSG = ""; // -1402;
+    public static final String ERR_LOGIN_MSG = "Login Error"; // -1501;
+    public static final String ERR_TIME_MSG = ""; // -1601;
+    public static final String ERR_TIME_SYS_MSG = ""; // -1602;
+    public static final String ERR_TIME_SAVE_MSG = ""; // -1603;
+    public static final String ERR_WIRELESS_MSG = ""; // -1701;
+    public static final String ERR_WIRELESS_UNSUPPORTED_MSG = ""; // -1702;
+    public static final String ERR_SCHEDULE_MSG = ""; // -1801;
+    public static final String ERR_SCHEDULE_FULL_MSG = ""; // -1802;
+    public static final String ERR_SCHEDULE_CONFLICT_MSG = ""; // -1803;
+    public static final String ERR_SCHEDULE_SAVE_MSG = ""; // -1804;
+    public static final String ERR_SCHEDULE_INDEX_MSG = ""; // -1805;
+    public static final String ERR_COUNTDOWN_MSG = ""; // -1901;
+    public static final String ERR_COUNTDOWN_CONFLICT_MSG = ""; // -1902;
+    public static final String ERR_COUNTDOWN_SAVE_MSG = ""; // -1903;
+    public static final String ERR_ANTITHEFT_MSG = ""; // -2001;
+    public static final String ERR_ANTITHEFT_CONFLICT_MSG = ""; // -2002;
+    public static final String ERR_ANTITHEFT_SAVE_MSG = ""; // -2003;
+    public static final String ERR_ACCOUNT_MSG = ""; // -2101;
+    public static final String ERR_STAT_MSG = ""; // -2201;
+    public static final String ERR_STAT_SAVE_MSG = ""; // -2202;
+    public static final String ERR_DST_MSG = ""; // -2301;
+    public static final String ERR_DST_SAVE_MSG = ""; // -2302;
+
+    // List of Binding-Error-Messages
+    public static final String ERR_HTTP_RESPONSE_MSG = "Invalid HTTP-Response"; // 9001
+    public static final String ERR_COOKIE_MSG = "Cookie Error"; // 9002
+    public static final String ERR_DEVICE_OFFLINE_MSG = "Device Offline"; // 9009
+    public static final String ERR_CREDENTIALS_MSG = "Invalid Request or Credentials";
+    public static final String ERR_CONNECT_TIMEOUT_MSG = "Connection Timeout - device not reachable";
+
+    // List of Config-Error-Messages
+    public static final String ERR_CONF_IP_MSG = "IP-Address not valid"; // 10001;
+    public static final String ERR_CONF_CREDENTIALS_MSG = "credentials not set (bridge)"; // 10002;
+    public static final String ERR_NO_BRIDGE_MSG = "no brigde configured"; // 10003;
+
+    /****************************************
+     * ErrorTypes
+     ****************************************/
+    // communication errors - set device to offline (retry connect)
+    public static final Set<Integer> LIST_COMMUNICATION_ERRORS = Set.of(ERR_HTTP_RESPONSE, ERR_COOKIE,
+            ERR_DEVICE_OFFLINE, ERR_CONNECT_TIMEOUT);
+    // configuration errors - set device to state configuration error (don't retry)
+    public static final Set<Integer> LIST_CONFIGURATION_ERRORS = Set.of(ERR_CREDENTIALS);
+    // reauthenticate errors (trying login immediatly)
+    public static final Set<Integer> LIST_REAUTH_ERRORS = Set.of(ERR_SESSION_TIMEOUT, ERR_HAND_SHAKE_FAILED);
+}
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
new file mode 100644 (file)
index 0000000..faa2261
--- /dev/null
@@ -0,0 +1,153 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+***/
+package org.openhab.binding.tapocontrol.internal.constants;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link TapoBindingSettings} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Christian Wild - Initial contribution
+ ***/
+@NonNullByDefault
+public class TapoThingConstants {
+    public static final String DEVICE_VENDOR = "Tapo";
+
+    /*** LIST OF SUPPORTED DEVICE NAMES ***/
+    public static final String DEVICE_BRIDGE = "bridge";
+    public static final String DEVICE_P100 = "P100";
+    public static final String DEVICE_P105 = "P105";
+    public static final String DEVICE_L510E = "L510_Series";
+    public static final String DEVICE_L530E = "L530_Series";
+    public static final String DEVICE_L900 = "L900";
+    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_WHITE_BULB = "White-Light-Bulb";
+    public static final String DEVICE_DESCRIPTION_COLOR_BULB = "Color-Light-Bulb";
+    public static final String DEVICE_DESCRIPTION_LIGHTSTRIP = "LightStrip";
+
+    /*** LIST OF SUPPORTED THING UIDS ***/
+    public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_BRIDGE);
+    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 L510E_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L510E);
+    public static final ThingTypeUID L530E_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L530E);
+    public static final ThingTypeUID L900_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L900);
+    public static final ThingTypeUID UNIVERSAL_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_UNIVERSAL);
+
+    /*** SET OF SUPPORTED UIDS ***/
+    public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_UIDS = Set.of(BRIDGE_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_SMART_PLUG_UIDS = Set.of(P100_THING_TYPE, P105_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_WHITE_BULB_UIDS = Set.of(L510E_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_COLOR_BULB_UIDS = Set.of(L530E_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_LIGHT_STRIP_UIDS = Set.of(L900_THING_TYPE);
+    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
+            .unmodifiableSet(Stream
+                    .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_SMART_PLUG_UIDS, SUPPORTED_WHITE_BULB_UIDS,
+                            SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS)
+                    .flatMap(Set::stream).collect(Collectors.toSet()));
+
+    /*** THINGS WITH CHANNEL GROUPS ***/
+    public static final Set<ThingTypeUID> CHANNEL_GROUP_THING_SET = Collections
+            .unmodifiableSet(Stream
+                    .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_SMART_PLUG_UIDS, SUPPORTED_WHITE_BULB_UIDS,
+                            SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS)
+                    .flatMap(Set::stream).collect(Collectors.toSet()));
+
+    /*** DEVICE PROPERTY STRINGS (CLOUD) ***/
+    public static final String CLOUD_PROPERTY_ALIAS = "alias";
+    public static final String CLOUD_PROPERTY_FW = "fwVer";
+    public static final String CLOUD_PROPERTY_HW = "deviceHwVer";
+    public static final String CLOUD_PROPERTY_ID = "deviceId";
+    public static final String CLOUD_PROPERTY_MAC = "deviceMac";
+    public static final String CLOUD_PROPERTY_MODEL = "deviceName"; // use name cause modell returns different values
+    public static final String CLOUD_PROPERTY_NAME = "deviceName";
+    public static final String CLOUD_PROPERTY_REGION = "deviceRegion";
+    public static final String CLOUD_PROPERTY_SERVER_URL = "appServerUrl";
+    public static final String CLOUD_PROPERTY_TYPE = "deviceType";
+
+    /*** DEVICE PROPERTY STRINGS (DEVICE) ***/
+    public static final String DEVICE_PROPERTY_BRIGHTNES = "brightness";
+    public static final String DEVICE_PROPERTY_COLORTEMP = "color_temp";
+    public static final String DEVICE_PROPERTY_FW = "fw_ver";
+    public static final String DEVICE_PROPERTY_HUE = "hue";
+    public static final String DEVICE_PROPERTY_HW = "hw_ver";
+    public static final String DEVICE_PROPERTY_ID = "device_id";
+    public static final String DEVICE_PROPERTY_IP = "ip";
+    public static final String DEVICE_PROPERTY_MAC = "mac";
+    public static final String DEVICE_PROPERTY_MODEL = "model";
+    public static final String DEVICE_PROPERTY_NICKNAME = "nickname";
+    public static final String DEVICE_PROPERTY_ON = "device_on";
+    public static final String DEVICE_PROPERTY_ONTIME = "on_time";
+    public static final String DEVICE_PROPERTY_OVERHEAT = "overheated";
+    public static final String DEVICE_PROPERTY_REGION = "region";
+    public static final String DEVICE_PROPERTY_SATURATION = "saturation";
+    public static final String DEVICE_PROPERTY_SIGNAL = "signal_level";
+    public static final String DEVICE_PROPERTY_SIGNAL_RSSI = "rssi";
+    public static final String DEVICE_PROPERTY_TYPE = "type";
+    public static final String DEVICE_PROPERTY_USAGE_7 = "time_usage_past7";
+    public static final String DEVICE_PROPERTY_USAGE_30 = "time_usage_past30";
+    public static final String DEVICE_PROPERTY_USAGE_TODAY = "time_usage_today";
+    public static final String DEVICE_REPRASENTATION_PROPERTY = "macAddress";
+    // lightning effects
+    public static final String DEVICE_PROPERTY_EFFECT = "lighting_effect";
+    public static final String PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS = "brightness";
+    public static final String PROPERTY_LIGHTNING_EFFECT_COLORTEMPRANGE = "color_temp_range";
+    public static final String PROPERTY_LIGHTNING_EFFECT_CUSTOM = "custom";
+    public static final String PROPERTY_LIGHTNING_EFFECT_DISPLAYCOLORS = "displayColors";
+    public static final String PROPERTY_LIGHTNING_EFFECT_ENABLE = "enable";
+    public static final String PROPERTY_LIGHTNING_EFFECT_ID = "id";
+    public static final String PROPERTY_LIGHTNING_EFFECT_NAME = "name";
+
+    /*** DEVICE SETTINGS ***/
+    public static final Integer BULB_MIN_COLORTEMP = 2500;
+    public static final Integer BULB_MAX_COLORTEMP = 6500;
+
+    /*** CHANNEL LISTS ***/
+    // channel group actuator
+    public static final String CHANNEL_GROUP_ACTUATOR = "actuator";
+    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_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_ONTIME = "onTime";
+    public static final String CHANNEL_OVERHEAT = "overheated";
+    public static final String CHANNEL_WIFI_STRENGTH = "wifiSignal";
+    // channel group effect
+    public static final String CHANNEL_GROUP_EFFECTS = "effect";
+    public static final String CHANNEL_FX_BRIGHTNESS = "brightness";
+    public static final String CHANNEL_FX_COLORS = "displayColors";
+    public static final String CHANNEL_FX_CUSTOM = "custom";
+    public static final String CHANNEL_FX_ENABLE = "enable";
+    public static final String CHANNEL_FX_NAME = "name";
+
+    /*** LIST OF PROPERTY NAMES ***/
+    public static final String PROPERTY_FAMILY = "deviceFamily";
+    public static final String PROPERTY_LOCATION = "location";
+    public static final String PROPERTY_WIFI_LEVEL = "signal-strength";
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java
new file mode 100644 (file)
index 0000000..7feb214
--- /dev/null
@@ -0,0 +1,301 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.device;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.TapoDiscoveryService;
+import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+
+/**
+ * The {@link TapoBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels with a bridge.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBridgeHandler extends BaseBridgeHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
+    private final TapoErrorHandler bridgeError = new TapoErrorHandler();
+    private final TapoBridgeConfiguration config;
+    private final HttpClient httpClient;
+    private @Nullable ScheduledFuture<?> startupJob;
+    private @Nullable ScheduledFuture<?> pollingJob;
+    private @Nullable ScheduledFuture<?> discoveryJob;
+    private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
+    private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
+    private TapoCredentials credentials;
+
+    private String uid;
+
+    public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
+        super(bridge);
+        Thing thing = getThing();
+        this.cloudConnector = new TapoCloudConnector(this, httpClient);
+        this.config = new TapoBridgeConfiguration(thing);
+        this.credentials = new TapoCredentials();
+        this.uid = thing.getUID().toString();
+        this.httpClient = httpClient;
+    }
+
+    /***********************************
+     *
+     * BRIDGE INITIALIZATION
+     *
+     ************************************/
+    @Override
+    /**
+     * INIT BRIDGE
+     * set credentials and login cloud
+     */
+    public void initialize() {
+        this.config.loadSettings();
+        this.credentials = new TapoCredentials(config.username, config.password);
+        activateBridge();
+    }
+
+    /**
+     * ACTIVATE BRIDGE
+     */
+    private void activateBridge() {
+        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+        updateStatus(ThingStatus.UNKNOWN);
+
+        // background initialization (delay it a little bit):
+        this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
+    }
+
+    @Override
+    public void dispose() {
+        stopScheduler(this.startupJob);
+        stopScheduler(this.pollingJob);
+        stopScheduler(this.discoveryJob);
+        super.dispose();
+    }
+
+    /**
+     * ACTIVATE DISCOVERY SERVICE
+     */
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(TapoDiscoveryService.class);
+    }
+
+    /**
+     * Set DiscoveryService
+     * 
+     * @param discoveryService
+     */
+    public void setDiscoveryService(TapoDiscoveryService discoveryService) {
+        this.discoveryService = discoveryService;
+    }
+
+    /***********************************
+     *
+     * SCHEDULER
+     *
+     ************************************/
+
+    /**
+     * delayed OneTime StartupJob
+     */
+    private void delayedStartUp() {
+        loginCloud();
+        startCloudScheduler();
+        startDiscoveryScheduler();
+    }
+
+    /**
+     * Start CloudLogin Scheduler
+     */
+    protected void startCloudScheduler() {
+        Integer pollingInterval = config.cloudReconnectIntervalM;
+        if (pollingInterval > 0) {
+            logger.trace("{} starting bridge cloud sheduler", this.uid);
+
+            this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
+                    TimeUnit.MINUTES);
+        } else {
+            stopScheduler(this.pollingJob);
+        }
+    }
+
+    /**
+     * Start DeviceDiscovery Scheduler
+     */
+    protected void startDiscoveryScheduler() {
+        Integer pollingInterval = config.discoveryIntervalM;
+        if (config.cloudDiscoveryEnabled && pollingInterval > 0) {
+            logger.trace("{} starting bridge discovery sheduler", this.uid);
+
+            this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval,
+                    TimeUnit.MINUTES);
+        } else {
+            stopScheduler(this.discoveryJob);
+        }
+    }
+
+    /**
+     * Stop scheduler
+     * 
+     * @param scheduler ScheduledFeature<?> which schould be stopped
+     */
+    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+        if (scheduler != null) {
+            scheduler.cancel(true);
+            scheduler = null;
+        }
+    }
+
+    /***********************************
+     *
+     * ERROR HANDLER
+     *
+     ************************************/
+    /**
+     * return device Error
+     * 
+     * @return
+     */
+    public TapoErrorHandler getError() {
+        return this.bridgeError;
+    }
+
+    /**
+     * set device error
+     * 
+     * @param tapoError TapoErrorHandler-Object
+     */
+    public void setError(TapoErrorHandler tapoError) {
+        this.bridgeError.set(tapoError);
+    }
+
+    /***********************************
+     *
+     * BRIDGE COMMUNICATIONS
+     *
+     ************************************/
+
+    /**
+     * Login to Cloud
+     * 
+     * @return
+     */
+    public boolean loginCloud() {
+        bridgeError.reset(); // reset ErrorHandler
+        if (!config.username.isBlank() && !config.password.isBlank()) {
+            logger.debug("{} login with user {}", this.uid, config.username);
+            if (cloudConnector.login(config.username, config.password)) {
+                updateStatus(ThingStatus.ONLINE);
+                return true;
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
+            }
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set");
+        }
+        return false;
+    }
+
+    /***********************************
+     *
+     * DEVICE DISCOVERY
+     *
+     ************************************/
+
+    /**
+     * START DEVICE DISCOVERY
+     */
+    public void discoverDevices() {
+        this.discoveryService.startScan();
+    }
+
+    /**
+     * GET DEVICELIST CONNECTED TO BRIDGE
+     * 
+     * @return devicelist
+     */
+    public JsonArray getDeviceList() {
+        JsonArray deviceList = new JsonArray();
+        if (config.cloudDiscoveryEnabled) {
+            logger.trace("{} discover devicelist from cloud", this.uid);
+            deviceList = getDeviceListCloud();
+        }
+        return deviceList;
+    }
+
+    /**
+     * GET DEVICELIST FROM CLOUD
+     * returns all devices stored in cloud
+     * 
+     * @return deviceList from cloud
+     */
+    private JsonArray getDeviceListCloud() {
+        logger.trace("{} getDeviceList from cloud", this.uid);
+        bridgeError.reset(); // reset ErrorHandler
+        JsonArray deviceList = new JsonArray();
+        if (loginCloud()) {
+            deviceList = this.cloudConnector.getDeviceList();
+        }
+        return deviceList;
+    }
+
+    /***********************************
+     *
+     * BRIDGE GETTERS
+     *
+     ************************************/
+
+    public TapoCredentials getCredentials() {
+        return this.credentials;
+    }
+
+    public HttpClient getHttpClient() {
+        return this.httpClient;
+    }
+
+    public ThingUID getUID() {
+        return getThing().getUID();
+    }
+
+    public TapoBridgeConfiguration getBridgeConfig() {
+        return this.config;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoDevice.java
new file mode 100644 (file)
index 0000000..7c63ed9
--- /dev/null
@@ -0,0 +1,479 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.device;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceConfiguration;
+import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class as base for TAPO-Device device implementations.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public abstract class TapoDevice extends BaseThingHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoDevice.class);
+    protected final TapoErrorHandler deviceError = new TapoErrorHandler();
+    protected final String uid;
+    protected TapoDeviceConfiguration config;
+    protected TapoDeviceInfo deviceInfo;
+    protected @Nullable ScheduledFuture<?> startupJob;
+    protected @Nullable ScheduledFuture<?> pollingJob;
+    protected @NonNullByDefault({}) TapoDeviceConnector connector;
+    protected @NonNullByDefault({}) TapoBridgeHandler bridge;
+
+    /**
+     * Constructor
+     *
+     * @param thing Thing object representing device
+     */
+    protected TapoDevice(Thing thing) {
+        super(thing);
+        this.config = new TapoDeviceConfiguration(thing);
+        this.deviceInfo = new TapoDeviceInfo();
+        this.uid = getThing().getUID().getAsString();
+    }
+
+    /***********************************
+     *
+     * INIT AND SETTINGS
+     *
+     ************************************/
+
+    /**
+     * INITIALIZE DEVICE
+     */
+    @Override
+    public void initialize() {
+        try {
+            this.config.loadSettings();
+            Bridge bridgeThing = getBridge();
+            if (bridgeThing != null) {
+                BridgeHandler bridgeHandler = bridgeThing.getHandler();
+                if (bridgeHandler != null) {
+                    this.bridge = (TapoBridgeHandler) bridgeHandler;
+                    this.connector = new TapoDeviceConnector(this, bridge);
+                }
+            }
+        } catch (Exception e) {
+            logger.debug("({}) configuration error : {}", uid, e.getMessage());
+        }
+        TapoErrorHandler configError = checkSettings();
+        if (!configError.hasError()) {
+            activateDevice();
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
+        }
+    }
+
+    /**
+     * DISPOSE
+     */
+    @Override
+    public void dispose() {
+        try {
+            stopScheduler(this.startupJob);
+            stopScheduler(this.pollingJob);
+            connector.logout();
+        } catch (Exception e) {
+            // handle exception
+        }
+        super.dispose();
+    }
+
+    /**
+     * ACTIVATE DEVICE
+     */
+    private void activateDevice() {
+        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+        updateStatus(ThingStatus.UNKNOWN);
+
+        // background initialization (delay it a little bit):
+        this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
+        startScheduler();
+    }
+
+    /**
+     * CHECK SETTINGS
+     * 
+     * @return TapoErrorHandler with configuration-errors
+     */
+    protected TapoErrorHandler checkSettings() {
+        TapoErrorHandler configErr = new TapoErrorHandler();
+
+        /* check bridge */
+        if (bridge == null || !(bridge instanceof TapoBridgeHandler)) {
+            configErr.raiseError(ERR_NO_BRIDGE);
+            return configErr;
+        }
+        /* check ip-address */
+        if (!config.ipAddress.matches(IPV4_REGEX)) {
+            configErr.raiseError(ERR_CONF_IP);
+            return configErr;
+        }
+        /* check credentials */
+        if (!bridge.getCredentials().areSet()) {
+            configErr.raiseError(ERR_CONF_CREDENTIALS);
+            return configErr;
+        }
+        return configErr;
+    }
+
+    /**
+     * Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
+     *
+     * @throws IOException if an error code was set in the response object
+     */
+    protected void checkErrors() throws IOException {
+        final Integer errorCode = deviceError.getCode();
+
+        if (errorCode != 0) {
+            throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
+        }
+    }
+
+    /***********************************
+     *
+     * SCHEDULER
+     *
+     ************************************/
+    /**
+     * delayed OneTime StartupJob
+     */
+    private void delayedStartUp() {
+        connect();
+    }
+
+    /**
+     * Start scheduler
+     */
+    protected void startScheduler() {
+        Integer pollingInterval = this.config.pollingInterval;
+
+        if (pollingInterval > 0) {
+            if (pollingInterval < POLLING_MIN_INTERVAL_S) {
+                pollingInterval = POLLING_MIN_INTERVAL_S;
+            }
+            logger.trace("({}) starScheduler: create job with interval : {}", uid, pollingInterval);
+            this.pollingJob = scheduler.scheduleWithFixedDelay(this::schedulerAction, pollingInterval, pollingInterval,
+                    TimeUnit.SECONDS);
+        } else {
+            stopScheduler(this.pollingJob);
+        }
+    }
+
+    /**
+     * Stop scheduler
+     * 
+     * @param scheduler ScheduledFeature<?> which schould be stopped
+     */
+    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+        if (scheduler != null) {
+            scheduler.cancel(true);
+            scheduler = null;
+        }
+    }
+
+    /**
+     * Scheduler Action
+     */
+    protected void schedulerAction() {
+        logger.trace("({}) schedulerAction", uid);
+        queryDeviceInfo();
+    }
+
+    /***********************************
+     *
+     * ERROR HANDLER
+     *
+     ************************************/
+    /**
+     * return device Error
+     * 
+     * @return
+     */
+    public TapoErrorHandler getError() {
+        return this.deviceError;
+    }
+
+    /**
+     * set device error
+     * 
+     * @param tapoError TapoErrorHandler-Object
+     */
+    public void setError(TapoErrorHandler tapoError) {
+        this.deviceError.set(tapoError);
+        handleConnectionState();
+    }
+
+    /***********************************
+     *
+     * THING
+     *
+     ************************************/
+
+    /***
+     * Check if ThingType is model
+     * 
+     * @param model
+     * @return
+     */
+    protected Boolean isThingModel(String model) {
+        try {
+            ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model);
+            ThingTypeUID expectedType = getThing().getThingTypeUID();
+            return expectedType.equals(foundType);
+        } catch (Exception e) {
+            logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
+     * Compare MAC-Adress
+     * 
+     * @param deviceInfo
+     * @return true if is the expected device
+     */
+    protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
+        try {
+            String expectedThingUID = getThing().getProperties().get(DEVICE_REPRASENTATION_PROPERTY);
+            String foundThingUID = deviceInfo.getRepresentationProperty();
+            String foundModel = deviceInfo.getModel();
+            if (expectedThingUID == null || expectedThingUID.isBlank()) {
+                return isThingModel(foundModel);
+            }
+            /* sometimes received mac was with and sometimes without "-" from device */
+            expectedThingUID = unformatMac(expectedThingUID);
+            foundThingUID = unformatMac(foundThingUID);
+            return expectedThingUID.equals(foundThingUID);
+        } catch (Exception e) {
+            logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Return ThingUID
+     */
+    public ThingUID getThingUID() {
+        return getThing().getUID();
+    }
+
+    /***********************************
+     *
+     * DEVICE PROPERTIES
+     *
+     ************************************/
+
+    /**
+     * query device Properties
+     */
+    public void queryDeviceInfo() {
+        queryDeviceInfo(false);
+    }
+
+    /**
+     * query device Properties
+     * 
+     * @param ignoreGap ignore gap to last query. query anyway (force)
+     */
+    public void queryDeviceInfo(boolean ignoreGap) {
+        deviceError.reset();
+        if (connector.loggedIn()) {
+            connector.queryInfo(ignoreGap);
+        } else {
+            logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
+            connect();
+        }
+    }
+
+    /**
+     * SET DEVICE INFOs to device
+     * 
+     * @param deviceInfo
+     */
+    public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
+        this.deviceInfo = deviceInfo;
+        if (isExpectedThing(deviceInfo)) {
+            devicePropertiesChanged(deviceInfo);
+            handleConnectionState();
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
+                            + "'. Check IP-Address");
+        }
+    }
+
+    /**
+     * Handle full responsebody received from connector
+     * 
+     * @param responseBody
+     */
+    public void responsePasstrough(String responseBody) {
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * If only one property must be changed, there is also a convenient method
+     * updateProperty(String name, String value).
+     * 
+     * @param TapoDeviceInfo
+     */
+    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
+        /* device properties */
+        Map<String, String> properties = editProperties();
+        properties.put(Thing.PROPERTY_MAC_ADDRESS, deviceInfo.getMAC());
+        properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceInfo.getFirmwareVersion());
+        properties.put(Thing.PROPERTY_HARDWARE_VERSION, deviceInfo.getHardwareVersion());
+        properties.put(Thing.PROPERTY_MODEL_ID, deviceInfo.getModel());
+        properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceInfo.getSerial());
+        updateProperties(properties);
+    }
+
+    /**
+     * update channel state
+     * 
+     * @param channelID
+     * @param value
+     */
+    public void publishState(String channelID, State value) {
+        updateState(channelID, value);
+    }
+
+    /***********************************
+     *
+     * CONNECTION
+     *
+     ************************************/
+
+    /**
+     * Connect (login) to device
+     * 
+     */
+    public Boolean connect() {
+        deviceError.reset();
+        Boolean loginSuccess = false;
+
+        try {
+            loginSuccess = connector.login();
+            if (loginSuccess) {
+                connector.queryInfo();
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
+            }
+        } catch (Exception e) {
+            updateStatus(ThingStatus.UNKNOWN);
+        }
+        return loginSuccess;
+    }
+
+    /**
+     * disconnect device
+     */
+    public void disconnect() {
+        connector.logout();
+    }
+
+    /**
+     * handle device state by connector error
+     */
+    public void handleConnectionState() {
+        ThingStatus deviceState = getThing().getStatus();
+        Integer errorCode = deviceError.getCode();
+
+        if (errorCode == 0) {
+            if (deviceState != ThingStatus.ONLINE) {
+                updateStatus(ThingStatus.ONLINE);
+            }
+        } else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
+            connect();
+        } else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
+            disconnect();
+        } else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
+        } else {
+            updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
+        }
+    }
+
+    /**
+     * Return IP-Address of device
+     */
+    public String getIpAddress() {
+        return this.config.ipAddress;
+    }
+
+    /***********************************
+     *
+     * CHANNELS
+     *
+     ************************************/
+    /**
+     * Get ChannelID including group
+     * 
+     * @param group String channel-group
+     * @param channel String channel-name
+     * @return String channelID
+     */
+    protected String getChannelID(String group, String channel) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+        if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
+            return group + "#" + channel;
+        }
+        return channel;
+    }
+
+    /**
+     * Get Channel from ChannelID
+     * 
+     * @param channelID String channelID
+     * @return String channel-name
+     */
+    protected String getChannelFromID(ChannelUID channelID) {
+        String channel = channelID.getIdWithoutGroup();
+        channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
+        channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
+        channel = channel.replace(CHANNEL_GROUP_EFFECTS + "#", "");
+        return channel;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoLightStrip.java
new file mode 100644 (file)
index 0000000..d1dfc13
--- /dev/null
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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(DEVICE_PROPERTY_ON, command == OnOffType.ON);
+                    refreshInfo = true;
+                    break;
+                case CHANNEL_BRIGHTNESS:
+                    if (command instanceof PercentType) {
+                        Float percent = ((PercentType) command).floatValue();
+                        setBrightness(percent.intValue()); // 0..100% = 0..100
+                        refreshInfo = true;
+                    } else if (command instanceof DecimalType) {
+                        setBrightness(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR_TEMP:
+                    if (command instanceof DecimalType) {
+                        setColorTemp(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR:
+                    if (command instanceof HSBType) {
+                        setColor((HSBType) command);
+                        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(DEVICE_PROPERTY_ON, false);
+        } else {
+            HashMap<String, Object> newState = new HashMap<>();
+            newState.put(DEVICE_PROPERTY_ON, true);
+            newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
+            connector.sendDeviceCommands(newState);
+        }
+    }
+
+    /**
+     * SET COLOR
+     * 
+     * @param command
+     */
+    protected void setColor(HSBType command) {
+        HashMap<String, Object> newState = new HashMap<>();
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_HUE, command.getHue());
+        newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
+        newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
+        connector.sendDeviceCommands(newState);
+    }
+
+    /**
+     * SET COLORTEMP
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        HashMap<String, Object> newState = new HashMap<>();
+        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_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) {
+                    Float percent = ((PercentType) command).floatValue();
+                    lightEffect.setBrightness(percent.intValue()); // 0..100% = 0..100
+                } else if (command instanceof DecimalType) {
+                    lightEffect.setBrightness(((DecimalType) command).intValue());
+                }
+                break;
+            case CHANNEL_FX_COLORS:
+                // comming soon
+                break;
+            case CHANNEL_FX_NAME:
+                lightEffect.setName(command.toString());
+                break;
+            case CHANNEL_FX_ENABLE:
+                lightEffect.setEnable(command == OnOffType.ON);
+                break;
+        }
+        setLightEffects(lightEffect);
+    }
+
+    /**
+     * SET LIGHTNING EFFECTS
+     * 
+     * @param lightEffect new lightEffect
+     */
+    protected void setLightEffects(TapoLightEffect lightEffect) {
+        JsonObject newEffect = new JsonObject();
+        newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_ENABLE, lightEffect.getEnable());
+        newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_NAME, lightEffect.getName());
+        newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS, lightEffect.getBrightness());
+        newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_COLORTEMPRANGE, lightEffect.getColorTempRange().toString());
+        newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_DISPLAYCOLORS, lightEffect.getDisplayColors().toString());
+        newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_CUSTOM, lightEffect.getCustom());
+
+        connector.sendDeviceCommand(DEVICE_PROPERTY_EFFECT, newEffect.toString());
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * @param 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),
+                getQuantityType(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()));
+        publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_ENABLE), getOnOffType(lightEffect.getEnable()));
+        publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_CUSTOM), getOnOffType(lightEffect.getCustom()));
+    }
+}
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
new file mode 100644 (file)
index 0000000..e23aab3
--- /dev/null
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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 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(DEVICE_PROPERTY_ON, command == OnOffType.ON);
+                    refreshInfo = true;
+                    break;
+                case CHANNEL_BRIGHTNESS:
+                    if (command instanceof PercentType) {
+                        Float percent = ((PercentType) command).floatValue();
+                        setBrightness(percent.intValue()); // 0..100% = 0..100
+                        refreshInfo = true;
+                    } else if (command instanceof DecimalType) {
+                        setBrightness(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR_TEMP:
+                    if (command instanceof DecimalType) {
+                        setColorTemp(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR:
+                    if (command instanceof HSBType) {
+                        setColor((HSBType) command);
+                        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(DEVICE_PROPERTY_ON, false);
+        } else {
+            HashMap<String, Object> newState = new HashMap<>();
+            newState.put(DEVICE_PROPERTY_ON, true);
+            newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
+            connector.sendDeviceCommands(newState);
+        }
+    }
+
+    /**
+     * SET COLOR
+     * 
+     * @param command
+     */
+    protected void setColor(HSBType command) {
+        HashMap<String, Object> newState = new HashMap<>();
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_HUE, command.getHue());
+        newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
+        newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
+        connector.sendDeviceCommands(newState);
+    }
+
+    /**
+     * SET COLORTEMP
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        HashMap<String, Object> newState = new HashMap<>();
+        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_COLORTEMP, colorTemp);
+        connector.sendDeviceCommands(newState);
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * @param TapoDeviceInfo
+     */
+    @Override
+    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
+        super.devicePropertiesChanged(deviceInfo);
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceInfo.getBrightness()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+                getDecimalType(deviceInfo.getColorTemp()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceInfo.getSignalLevel()));
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getQuantityType(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/TapoSmartPlug.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/device/TapoSmartPlug.java
new file mode 100644 (file)
index 0000000..170dd8e
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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;
+
+        /* perform actions */
+        if (command instanceof RefreshType) {
+            refreshInfo = true;
+        } else if (command == OnOffType.ON) {
+            connector.sendDeviceCommand(DEVICE_PROPERTY_ON, true);
+            refreshInfo = true;
+        } else if (command == OnOffType.OFF) {
+            connector.sendDeviceCommand(DEVICE_PROPERTY_ON, false);
+            refreshInfo = true;
+        } else {
+            logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
+                    channelUID.getId());
+        }
+
+        /* refreshInfo */
+        if (refreshInfo) {
+            queryDeviceInfo(true);
+        }
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * @param TapoDeviceInfo
+     */
+    @Override
+    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
+        super.devicePropertiesChanged(deviceInfo);
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceInfo.getSignalLevel()));
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getQuantityType(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
new file mode 100644 (file)
index 0000000..b45f105
--- /dev/null
@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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(DEVICE_PROPERTY_ON, command == OnOffType.ON);
+                    refreshInfo = true;
+                    break;
+                case CHANNEL_BRIGHTNESS:
+                    if (command instanceof PercentType) {
+                        Float percent = ((PercentType) command).floatValue();
+                        setBrightness(percent.intValue()); // 0..100% = 0..100
+                        refreshInfo = true;
+                    } else if (command instanceof DecimalType) {
+                        setBrightness(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR_TEMP:
+                    if (command instanceof DecimalType) {
+                        setColorTemp(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR:
+                    if (command instanceof HSBType) {
+                        setColor((HSBType) command);
+                        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(DEVICE_PROPERTY_ON, false);
+        } else {
+            HashMap<String, Object> newState = new HashMap<>();
+            newState.put(DEVICE_PROPERTY_ON, true);
+            newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
+            connector.sendDeviceCommands(newState);
+        }
+    }
+
+    /**
+     * SET COLOR
+     * 
+     * @param command
+     */
+    protected void setColor(HSBType command) {
+        HashMap<String, Object> newState = new HashMap<>();
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_HUE, command.getHue());
+        newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
+        newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
+        connector.sendDeviceCommands(newState);
+    }
+
+    /**
+     * SET COLORTEMP
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        HashMap<String, Object> newState = new HashMap<>();
+        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_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
+     */
+    public void responsePasstrough(String responseBody) {
+        logger.debug("({}) received response {}", uid, responseBody);
+        publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * @param TapoDeviceInfo
+     */
+    @Override
+    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
+        super.devicePropertiesChanged(deviceInfo);
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceInfo.getBrightness()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+                getDecimalType(deviceInfo.getColorTemp()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
+
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceInfo.getSignalLevel()));
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getQuantityType(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
+     */
+    protected String getChannelFromID(ChannelUID channelID) {
+        String channel = channelID.getIdWithoutGroup();
+        channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
+        channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
+        channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
+        return channel;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/MimeEncode.java
new file mode 100644 (file)
index 0000000..9a6f6da
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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
new file mode 100644 (file)
index 0000000..aa35002
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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) {
+            this.parameters.addProperty(name, (Boolean) value);
+        } else if (value instanceof Number) {
+            this.parameters.addProperty(name, (Number) value);
+        } 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);
+        payload.add("params", this.parameters);
+        payload.addProperty("requestTimeMils", timeMils);
+
+        return payload;
+    }
+
+    /**
+     * Flush Parameters
+     * remove all parameters
+     */
+    public void flushParameters(String command) {
+        this.parameters = new JsonObject();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/helpers/TapoCipher.java
new file mode 100644 (file)
index 0000000..7923ee1
--- /dev/null
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers;
+
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO-CIPHER
+ * Based on K4CZP3R's p100-java-poc
+ * 
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class TapoCipher {
+    private final Logger logger = LoggerFactory.getLogger(TapoCipher.class);
+    protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+    protected static final String CIPHER_ALGORITHM = "AES";
+    protected static final String CIPHER_CHARSET = "UTF-8";
+    protected static final String HANDSHAKE_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
+    protected static final String HANDSHAKE_ALGORITHM = "RSA";
+    protected static final String HANDSHAKE_CHARSET = "UTF-8";
+
+    @NonNullByDefault({})
+    private Cipher encodeCipher;
+    @NonNullByDefault({})
+    private Cipher decodeCipher;
+    @NonNullByDefault({})
+    private MimeEncode mimeEncode;
+
+    /**
+     * CREATE NEW EMPTY CIPHER
+     */
+    public TapoCipher() {
+    }
+
+    /**
+     * CREATE NEW CIPHER WITH KEY AND CREDENTIALS
+     * 
+     * @param handshakeKey Key from Handshake-Request
+     * @param credentials TapoCredentials
+     * @throws Exception
+     */
+    public TapoCipher(String handshakeKey, TapoCredentials credentials) {
+        setKey(handshakeKey, credentials);
+    }
+
+    /**
+     * SET NEW KEY AND CREDENTIALS
+     * 
+     * @param handshakeKey
+     * @param credentials
+     */
+    public void setKey(String handshakeKey, TapoCredentials credentials) {
+        logger.trace("Init TapoCipher with key: {} ", handshakeKey);
+        MimeEncode mimeEncode = new MimeEncode();
+        try {
+            byte[] decode = mimeEncode.decode(handshakeKey.getBytes(HANDSHAKE_CHARSET));
+            byte[] decode2 = mimeEncode.decode(credentials.getPrivateKeyBytes());
+            Cipher instance = Cipher.getInstance(HANDSHAKE_TRANSFORMATION);
+            KeyFactory kf = KeyFactory.getInstance(HANDSHAKE_ALGORITHM);
+            PrivateKey p = kf.generatePrivate(new PKCS8EncodedKeySpec(decode2));
+            instance.init(Cipher.DECRYPT_MODE, p);
+            byte[] doFinal = instance.doFinal(decode);
+            byte[] bArr = new byte[16];
+            byte[] bArr2 = new byte[16];
+            System.arraycopy(doFinal, 0, bArr, 0, 16);
+            System.arraycopy(doFinal, 16, bArr2, 0, 16);
+            initCipher(bArr, bArr2);
+        } catch (Exception ex) {
+            logger.warn("Something went wrong: {}", ex.getMessage());
+        }
+    }
+
+    /**
+     * INIT ENCODE/DECDE-CIPHERS
+     * 
+     * @param bArr
+     * @param bArr2
+     * @throws Exception
+     */
+    protected void initCipher(byte[] bArr, byte[] bArr2) throws Exception {
+        try {
+            mimeEncode = new MimeEncode();
+            SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, CIPHER_ALGORITHM);
+            IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
+            this.encodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            this.decodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            this.encodeCipher.init(1, secretKeySpec, ivParameterSpec);
+            this.decodeCipher.init(2, secretKeySpec, ivParameterSpec);
+        } catch (Exception e) {
+            logger.warn("initChiper failed: {}", e.getMessage());
+            this.encodeCipher = null;
+            this.decodeCipher = null;
+        }
+    }
+
+    /**
+     * ENCODE STRING
+     * 
+     * @param str source string to encode
+     * @return encoded string
+     * @throws Exception
+     */
+    public String encode(String str) throws Exception {
+        byte[] doFinal;
+        doFinal = this.encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET));
+        String encrypted = mimeEncode.encodeToString(doFinal);
+        return encrypted.replace("\r\n", "");
+    }
+
+    /**
+     * DECODE STRING
+     * 
+     * @param str source string to decode
+     * @return decoded string
+     * @throws Exception
+     */
+    public String decode(String str) throws Exception {
+        byte[] data = mimeEncode.decode(str.getBytes(CIPHER_CHARSET));
+        byte[] doFinal;
+        doFinal = this.decodeCipher.doFinal(data);
+        return new String(doFinal);
+    }
+}
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
new file mode 100644 (file)
index 0000000..416905e
--- /dev/null
@@ -0,0 +1,220 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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
+ *
+ * @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 = "";
+
+    /**
+     * INIT CLASS
+     *
+     */
+    public TapoCredentials() {
+        this.mimeEncoder = new MimeEncode();
+    }
+
+    /**
+     * INIT CLASS
+     *
+     * @param email E-Mail-adress of Tapo Cloud
+     * @param passowrd Password of Tapo Cloud
+     */
+    public TapoCredentials(String eMail, String password) {
+        this.mimeEncoder = new MimeEncode();
+        setCredectials(eMail, password);
+    }
+
+    /**
+     * set credentials.
+     *
+     * @param username username (eMail-adress) of Tapo Cloud
+     * @param passowrd 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];
+        }
+    }
+
+    /**
+     * CHECK IF CREDENTIALS ARE SET
+     * 
+     * @return
+     */
+    public Boolean areSet() {
+        return !(this.username.isEmpty() || this.password.isEmpty());
+    }
+}
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
new file mode 100644 (file)
index 0000000..78f70ae
--- /dev/null
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.lang.reflect.Field;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * Class Handling TapoErrors
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoErrorHandler extends Exception {
+    private static final long serialVersionUID = 0L;
+    private Integer errorCode = 0;
+    private String errorMessage = "";
+    private String infoMessage = "";
+    private Gson gson = new Gson();
+
+    /**
+     * Constructor
+     *
+     */
+    public TapoErrorHandler() {
+    }
+
+    /**
+     * Constructor
+     * 
+     * @param errorCode error code (number)
+     */
+    public TapoErrorHandler(Integer errorCode) {
+        raiseError(errorCode);
+    }
+
+    /**
+     * Constructor
+     * 
+     * @param errorCode error code (number)
+     * @param infoMessage optional info-message
+     */
+    public TapoErrorHandler(Integer errorCode, String infoMessage) {
+        raiseError(errorCode, infoMessage);
+    }
+
+    /**
+     * Constructor
+     * 
+     * @param exception Exception
+     */
+    public TapoErrorHandler(Exception ex) {
+        raiseError(ex);
+    }
+
+    /**
+     * Constructor
+     * 
+     * @param exception Exception
+     * @param infoMessage optional info-message
+     */
+    public TapoErrorHandler(Exception ex, String infoMessage) {
+        raiseError(ex, infoMessage);
+    }
+
+    /***********************************
+     *
+     * Private Functions
+     *
+     ************************************/
+
+    /**
+     * GET ERROR-MESSAGE
+     * 
+     * @param errCode error Number (or constant ERR_CODE )
+     * @return error-message if set constant ERR_CODE_MSG. if not name of ERR_CODE is returned
+     */
+    private String getErrorMessage(Integer errCode) {
+        Field[] fields = TapoErrorConstants.class.getDeclaredFields();
+        /* loop ErrorConstants and search for code in value */
+        for (Field f : fields) {
+            String constName = f.getName();
+            try {
+                Integer val = (Integer) f.get(this);
+                if (val != null && val.equals(errCode)) {
+                    Field constantName = TapoErrorConstants.class.getDeclaredField(constName + "_MSG");
+                    String msg = getValueOrDefault(constantName.get(null), "").toString();
+                    if (msg.length() > 2) {
+                        return msg;
+                    } else {
+                        return infoMessage + " (" + constName + ")";
+                    }
+                }
+            } catch (Exception e) {
+                // next loop
+            }
+        }
+        return infoMessage + " (" + errCode.toString() + ")";
+    }
+
+    /***********************************
+     *
+     * Public Functions
+     *
+     ************************************/
+
+    /**
+     * Raises new error
+     * 
+     * @param errorCode error code (number)
+     */
+    public void raiseError(Integer errorCode) {
+        raiseError(errorCode, "");
+    }
+
+    /**
+     * Raises new error
+     * 
+     * @param errorCode error code (number)
+     * @param infoMessage optional info-message
+     */
+    public void raiseError(Integer errorCode, String infoMessage) {
+        this.errorCode = errorCode;
+        this.infoMessage = infoMessage;
+        this.errorMessage = getErrorMessage(errorCode);
+    }
+
+    /**
+     * Raises new error
+     * 
+     * @param exception Exception
+     */
+    public void raiseError(Exception ex) {
+        raiseError(ex, "");
+    }
+
+    /**
+     * Raises new error
+     * 
+     * @param exception Exception
+     * @param infoMessage optional info-message
+     */
+    public void raiseError(Exception ex, String infoMessage) {
+        this.errorCode = ex.hashCode();
+        this.infoMessage = infoMessage;
+        this.errorMessage = getValueOrDefault(ex.getMessage(), ex.toString());
+    }
+
+    /**
+     * Take over tapoError
+     * 
+     * @param tapoError
+     */
+    public void set(TapoErrorHandler tapoError) {
+        this.errorCode = tapoError.getNumber();
+        this.infoMessage = tapoError.getExtendedInfo();
+        this.errorMessage = getErrorMessage(this.errorCode);
+    }
+
+    /**
+     * Reset Error
+     */
+    public void reset() {
+        this.errorCode = 0;
+        this.errorMessage = "";
+        this.infoMessage = "";
+    }
+
+    /***********************************
+     *
+     * GET RESULTS
+     *
+     ************************************/
+
+    /**
+     * Get Error Message
+     * 
+     * @return error text
+     */
+    @Override
+    @Nullable
+    public String getMessage() {
+        return this.errorMessage;
+    }
+
+    /**
+     * Get Error Message directly by error-number
+     * 
+     * @param errorCode
+     * @return error message
+     */
+    public String getMessage(Integer errorCode) {
+        return getErrorMessage(errorCode);
+    }
+
+    /**
+     * Get Error Code
+     * 
+     * @return error code (integer)
+     */
+    public Integer getCode() {
+        return this.errorCode;
+    }
+
+    /**
+     * Get Info Message
+     * 
+     * @return error extended info
+     */
+    public String getExtendedInfo() {
+        return this.infoMessage;
+    }
+
+    /**
+     * Get Error Number
+     * 
+     * @return error number
+     */
+    public Integer getNumber() {
+        return this.errorCode;
+    }
+
+    /**
+     * Check if has Error
+     * 
+     * @return true if has error
+     */
+    public Boolean hasError() {
+        return this.errorCode != 0;
+    }
+
+    /**
+     * Get JSON-Object with errror
+     * 
+     * @return JsonObject with error-informations
+     */
+    public JsonObject getJson() {
+        JsonObject json;
+        json = gson.fromJson("{'error_code': '" + errorCode + "', 'error_message':'" + errorMessage + "'}",
+                JsonObject.class);
+        if (json == null) {
+            json = new JsonObject();
+        }
+        return json;
+    }
+}
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
new file mode 100644 (file)
index 0000000..1364ec7
--- /dev/null
@@ -0,0 +1,348 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Time;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * {@link TapoUtils} TapoUtils -
+ * Utility Helper Functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class TapoUtils {
+
+    /************************************
+     * CALCULATION UTILS
+     ***********************************/
+    /**
+     * Limit Value between limits
+     * 
+     * @param value Integer
+     * @param lowerLimit
+     * @param upperLimit
+     * @return
+     */
+    public static Integer limitVal(@Nullable Integer value, Integer lowerLimit, Integer upperLimit) {
+        if (value == null || value < lowerLimit) {
+            return lowerLimit;
+        } else if (value > upperLimit) {
+            return upperLimit;
+        }
+        return value;
+    }
+
+    /************************************
+     * FORMAT UTILS
+     ***********************************/
+    /**
+     * return value or default val if it's null
+     * 
+     * @param <T> Type of value
+     * @param value value
+     * @param defaultValue defaut value
+     * @return
+     */
+    public static <T> T getValueOrDefault(@Nullable T value, T defaultValue) {
+        return value == null ? defaultValue : value;
+    }
+
+    /**
+     * Format MAC-Address replacing old division chars and add new one
+     * 
+     * @param mac unformated mac-Address
+     * @param newDivisionChar new division char (e.g. ":","-" )
+     * @return new formated mac-Address
+     */
+    public static String formatMac(String mac, char newDivisionChar) {
+        String unformatedMac = unformatMac(mac);
+        String formatedMac = unformatedMac.replaceAll("(.{2})", "$1" + newDivisionChar).substring(0, 17);
+        return formatedMac;
+    }
+
+    /**
+     * 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 ? boolVal ? OnOffType.ON : OnOffType.OFF : OnOffType.OFF);
+    }
+
+    /**
+     * Return OnOffType from bool
+     * 
+     * @param boolVal
+     */
+    public static OnOffType getOnOffType(Integer intVal) {
+        return intVal == 0 ? OnOffType.OFF : OnOffType.ON;
+    }
+
+    /**
+     * 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 (Unit<Time>)
+     * @return QuantityTime<Time>
+     */
+    public static QuantityType<Time> getQuantityType(@Nullable Number numVal, Unit<Time> unit) {
+        return new QuantityType<>((numVal != null ? numVal : 0), unit);
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java
new file mode 100644 (file)
index 0000000..ef5b22b
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoBridgeConfiguration {
+    private final Logger logger = LoggerFactory.getLogger(TapoBridgeConfiguration.class);
+
+    /* THING CONFIGUTATION PROPERTYS */
+    public static final String CONFIG_EMAIL = "username";
+    public static final String CONFIG_PASS = "password";
+    public static final String CONFIG_DEVICE_IP = "ipAddress";
+    public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
+    public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
+    public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
+
+    /* DEFAULT & FIXED CONFIGURATIONS */
+    public static final Integer CONFIG_CLOUD_FIXED_INTERVAL = 1440;
+
+    /* thing configuration parameter. */
+    public String username = "";
+    public String password = "";
+    public Boolean cloudDiscoveryEnabled = false;
+    public Boolean udpDiscoveryEnabled = false;
+    public Integer cloudReconnectIntervalM = CONFIG_CLOUD_FIXED_INTERVAL;
+    public Integer discoveryIntervalM = 30;
+
+    private Thing bridge;
+
+    /**
+     * Create settings
+     * 
+     * @param thing BridgeThing
+     */
+    public TapoBridgeConfiguration(Thing thing) {
+        this.bridge = thing;
+        loadSettings();
+    }
+
+    /**
+     * LOAD SETTINGS
+     */
+    public void loadSettings() {
+        try {
+            Configuration config = this.bridge.getConfiguration();
+            username = config.get(CONFIG_EMAIL).toString();
+            password = config.get(CONFIG_PASS).toString();
+            cloudDiscoveryEnabled = Boolean.parseBoolean(config.get(CONFIG_DISCOVERY_CLOUD).toString());
+            discoveryIntervalM = Integer.valueOf(config.get(CONFIG_DISCOVERY_INTERVAL).toString());
+        } catch (Exception e) {
+            logger.warn("{} error reading configuration: '{}'", bridge.getUID(), e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceConfiguration.java
new file mode 100644 (file)
index 0000000..eaf4701
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoDeviceConfiguration {
+    private final Logger logger = LoggerFactory.getLogger(TapoDeviceConfiguration.class);
+
+    /* 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 Integer pollingInterval = 30;
+
+    private final Thing device;
+
+    /**
+     * Create settings
+     * 
+     * @param thing BridgeThing
+     */
+    public TapoDeviceConfiguration(Thing thing) {
+        this.device = thing;
+        loadSettings();
+    }
+
+    /**
+     * LOAD SETTINGS
+     */
+    public void loadSettings() {
+        try {
+            Configuration config = this.device.getConfiguration();
+            this.ipAddress = config.get(CONFIG_DEVICE_IP).toString();
+            this.pollingInterval = Integer.valueOf(config.get(CONFIG_UPDATE_INTERVAL).toString());
+        } catch (Exception e) {
+            logger.warn("{} error reading device-configuration: '{}'", device.getUID().toString(), e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java
new file mode 100644 (file)
index 0000000..641d357
--- /dev/null
@@ -0,0 +1,224 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDeviceInfo {
+    private Boolean deviceOn = false;
+    private Boolean overheated = false;
+    private Integer brightness = 0;
+    private Integer colorTemp = 0;
+    private Integer hue = 0;
+    private Integer rssi = 0;
+    private Integer saturation = 100;
+    private Integer signalLevel = 0;
+    private Number onTime = 0;
+    private Number timeUsagePast7 = 0;
+    private Number timeUsagePast30 = 0;
+    private Number timeUsageToday = 0;
+    private String deviceId = "";
+    private String fwVer = "";
+    private String hwVer = "";
+    private String ip = "";
+    private String mac = "";
+    private String model = "";
+    private String nickname = "";
+    private String region = "";
+    private String type = "";
+    private TapoLightEffect lightEffect = new TapoLightEffect();
+
+    private JsonObject jsonObject = new JsonObject();
+
+    /**
+     * INIT
+     */
+    public TapoDeviceInfo() {
+        setData();
+    }
+
+    /**
+     * Init DeviceInfo with new Data;
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoDeviceInfo(JsonObject jso) {
+        jsonObject = jso;
+        setData();
+    }
+
+    /**
+     * Set Data (new JsonObject)
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoDeviceInfo setData(JsonObject jso) {
+        this.jsonObject = jso;
+        setData();
+        return this;
+    }
+
+    private void setData() {
+        this.brightness = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_BRIGHTNES);
+        this.colorTemp = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_COLORTEMP, BULB_MIN_COLORTEMP);
+        this.deviceId = jsonObjectToString(jsonObject, DEVICE_PROPERTY_ID);
+        this.deviceOn = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_ON);
+        this.fwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_FW);
+        this.hue = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_HUE);
+        this.hwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_HW);
+        this.ip = jsonObjectToString(jsonObject, DEVICE_PROPERTY_IP);
+        this.lightEffect = lightEffect.setData(jsonObject);
+        this.mac = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MAC);
+        this.model = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MODEL);
+        this.nickname = jsonObjectToString(jsonObject, DEVICE_PROPERTY_NICKNAME);
+        this.onTime = jsonObjectToNumber(jsonObject, DEVICE_PROPERTY_ONTIME);
+        this.overheated = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_OVERHEAT);
+        this.region = jsonObjectToString(jsonObject, DEVICE_PROPERTY_REGION);
+        this.saturation = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SATURATION);
+        this.signalLevel = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL);
+        this.rssi = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL_RSSI);
+        this.timeUsagePast7 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_7);
+        this.timeUsagePast30 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_30);
+        this.timeUsageToday = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_TODAY);
+        this.type = jsonObjectToString(jsonObject, DEVICE_PROPERTY_TYPE);
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public Integer getBrightness() {
+        return brightness;
+    }
+
+    public Integer getColorTemp() {
+        return colorTemp;
+    }
+
+    public String getFirmwareVersion() {
+        return fwVer;
+    }
+
+    public String getHardwareVersion() {
+        return hwVer;
+    }
+
+    public HSBType getHSB() {
+        DecimalType h = new DecimalType(hue);
+        PercentType s = new PercentType(saturation);
+        PercentType b = new PercentType(brightness);
+        return new HSBType(h, s, b);
+    }
+
+    public Integer getHue() {
+        return hue;
+    }
+
+    public TapoLightEffect getLightEffect() {
+        return lightEffect;
+    }
+
+    public String getIP() {
+        return ip;
+    }
+
+    public Boolean isOff() {
+        return !deviceOn;
+    }
+
+    public Boolean isOn() {
+        return deviceOn;
+    }
+
+    public Boolean isOverheated() {
+        return overheated;
+    }
+
+    public String getMAC() {
+        return formatMac(mac, MAC_DIVISION_CHAR);
+    }
+
+    public String getModel() {
+        return model.replace(" ", "_");
+    }
+
+    public String getNickname() {
+        return nickname;
+    }
+
+    public Number getOnTime() {
+        return onTime;
+    }
+
+    public String getRegion() {
+        return region;
+    }
+
+    public String getRepresentationProperty() {
+        return getMAC();
+    }
+
+    public Integer getSaturation() {
+        return saturation;
+    }
+
+    public String getSerial() {
+        return deviceId;
+    }
+
+    public Integer getSignalLevel() {
+        return signalLevel;
+    }
+
+    public Integer getRSSI() {
+        return rssi;
+    }
+
+    public Number getTimeUsagePast7() {
+        return timeUsagePast7;
+    }
+
+    public Number getTimeUsagePast30() {
+        return timeUsagePast30;
+    }
+
+    public Number getTimeUsagePastToday() {
+        return timeUsageToday;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public String toString() {
+        return jsonObject.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java b/bundles/org.openhab.binding.tapocontrol/src/main/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java
new file mode 100644 (file)
index 0000000..12876e7
--- /dev/null
@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.awt.Color;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Tapo-LightningEffect Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightEffect {
+    private Integer enable = 0;
+    private String id = "";
+    private String name = "";
+    private Integer custom = 0;
+    private Integer brightness = 0;
+    private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
+    private Color displayColors[] = { Color.WHITE };
+
+    private JsonObject jsonObject = new JsonObject();
+
+    /**
+     * INIT
+     */
+    public TapoLightEffect() {
+        setData();
+    }
+
+    /**
+     * Init DeviceInfo with new Data;
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoLightEffect(JsonObject jso) {
+        setData(jso);
+    }
+
+    /**
+     * Set Data (new JsonObject)
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoLightEffect setData(JsonObject jso) {
+        /* create empty jsonObject to set efault values if has no lighning effect */
+        if (jsonObject.has(DEVICE_PROPERTY_EFFECT)) {
+            this.jsonObject = jso;
+        } else {
+            jsonObject = new JsonObject();
+        }
+        setData();
+        return this;
+    }
+
+    private void setData() {
+        this.enable = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_ENABLE);
+        this.id = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_ID);
+        this.name = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_NAME);
+        this.custom = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_CUSTOM); // jsonObjectToBool
+        this.brightness = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS);
+    }
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    public void setEnable(Boolean enable) {
+        this.enable = enable ? 1 : 0;
+    }
+
+    public void setName(String value) {
+        this.name = value;
+    }
+
+    public void setCustom(Boolean enable) {
+        this.custom = enable ? 1 : 0;
+    }
+
+    public void setBrightness(Integer value) {
+        this.brightness = value;
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public Integer getEnable() {
+        return this.enable;
+    }
+
+    public String getId() {
+        return this.id;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public Integer getCustom() {
+        return this.custom;
+    }
+
+    public Integer getBrightness() {
+        return this.brightness;
+    }
+
+    public Integer[] getColorTempRange() {
+        return this.colorTempRange;
+    }
+
+    public Color[] getDisplayColors() {
+        return this.displayColors;
+    }
+
+    @Override
+    public String toString() {
+        return jsonObject.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..0c64a32
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="tapocontrol" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+       <name>TapoControl Binding</name>
+       <description>Control your TAPO-SmartHome Devices</description>
+</binding:binding>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..09b5ef5
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:tapo:device">
+               <parameter name="ipAddress" type="text" required="true">
+                       <context>network-address</context>
+                       <label>IP Address</label>
+               </parameter>
+               <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
+                       <label>Refresh Interval</label>
+                       <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
+                       <default>30</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+       <config-description uri="bridge-type:tapo:bridge">
+               <parameter name="username" type="text" required="true">
+                       <context>email</context>
+                       <label>Username</label>
+                       <description>Tapo-Cloud Login User (e-Mail)</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <context>password</context>
+                       <label>Password</label>
+                       <description>Tapo-Cloud Login Password</description>
+               </parameter>
+               <parameter name="cloudDiscovery" type="boolean" required="false">
+                       <label>Cloud Discovery</label>
+                       <description>Use Cloud Discovery-Service</description>
+                       <default>false</default>
+                       <advanced>false</advanced>
+               </parameter>
+               <parameter name="discoveryInterval" type="integer" min="1" max="10080" required="false">
+                       <label>Background Discovery Interval</label>
+                       <description>Interval background discovery in minutes (default 60)</description>
+                       <default>60</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L510_Series.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L510_Series.xml
new file mode 100644 (file)
index 0000000..ff96871
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- L510E THING-TYPE (WHITE-LIGHT-BULB) -->
+       <thing-type id="L510_Series">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>L510 Series White-Bulb</label>
+               <description>Tapo Smart dimmable White-Light-Bulb</description>
+               <channel-groups>
+                       <channel-group id="actuator" typeId="lightBulb"/>
+                       <channel-group id="device" typeId="deviceState"/>
+               </channel-groups>
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:tapo:device"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L530_Series.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L530_Series.xml
new file mode 100644 (file)
index 0000000..4f94a24
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- L530 Series THING-TYPE (COLOR-LIGHT-BULB) -->
+       <thing-type id="L530_Series">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>L530 Series Color-Bulb</label>
+               <description>Tapo Smart Multicolor Light-Bulb</description>
+               <channel-groups>
+                       <channel-group id="actuator" typeId="colorBulb"/>
+                       <channel-group id="device" typeId="deviceState"/>
+               </channel-groups>
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:tapo:device"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L900.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/L900.xml
new file mode 100644 (file)
index 0000000..36b2c06
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- L530 Series THING-TYPE (COLOR-LIGHT-BULB) -->
+       <thing-type id="L900">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>L900 LightStrip</label>
+               <description>Tapo Smart Multicolor Light-Lightstrip</description>
+               <channel-groups>
+                       <channel-group id="actuator" typeId="lightStrip"/>
+                       <channel-group id="device" typeId="deviceState"/>
+               </channel-groups>
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:tapo:device"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/P100.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/P100.xml
new file mode 100644 (file)
index 0000000..49f519c
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- P100 THING-TYPE (SOCKET) -->
+       <thing-type id="P100">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>P100 SmartPlug</label>
+               <description>Tapo Smart Wifi Plug</description>
+               <channel-groups>
+                       <channel-group id="actuator" typeId="smartPlug"/>
+                       <channel-group id="device" typeId="deviceState"/>
+               </channel-groups>
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:tapo:device"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/P105.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/P105.xml
new file mode 100644 (file)
index 0000000..f3819ce
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- P100 THING-TYPE (SOCKET) -->
+       <thing-type id="P105">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>P105 SmartPlug</label>
+               <description>Tapo Mini Smart Wifi Plug</description>
+               <channel-groups>
+                       <channel-group id="actuator" typeId="smartPlug"/>
+                       <channel-group id="device" typeId="deviceState"/>
+               </channel-groups>
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:tapo:device"/>
+       </thing-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/bridge.xml
new file mode 100644 (file)
index 0000000..7b035e8
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <bridge-type id="bridge">
+               <label>Cloud-Login</label>
+               <description>Cloud Connector. Acts as device-bridge</description>
+               <config-description-ref uri="bridge-type:tapo:bridge"/>
+       </bridge-type>
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.tapocontrol/src/main/resources/OH-INF/thing/channels.xml
new file mode 100644 (file)
index 0000000..ac305ed
--- /dev/null
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- ############################### CHANNEL-GROUPS ############################### -->
+
+       <!-- CHANNEL GROUP TYPES -->
+       <!--Device-Statuss Channel Type -->
+       <channel-group-type id="deviceState">
+               <label>Device State</label>
+               <description>Information about the device</description>
+               <channels>
+                       <channel id="wifiSignal" typeId="system.signal-strength"/>
+                       <channel id="onTime" typeId="ontime"/>
+                       <channel id="overheated" typeId="overheated"/>
+               </channels>
+       </channel-group-type>
+
+       <!--Actor Channel Type -->
+       <channel-group-type id="smartPlug">
+               <label>SmartPlug</label>
+               <description>Tapo Smart Plug Power Outlet</description>
+               <channels>
+                       <channel id="output" typeId="outputChannel"/>
+               </channels>
+       </channel-group-type>
+
+       <!--Light-Bulb Channel Type -->
+       <channel-group-type id="lightBulb">
+               <label>Light Bulb</label>
+               <description>Tapo Smart Light Bulb</description>
+               <channels>
+                       <channel id="output" typeId="lightOn"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+                       <channel id="colorTemperature" typeId="colorTemperature"/>
+               </channels>
+       </channel-group-type>
+
+       <!--Color Channel Type -->
+       <channel-group-type id="colorBulb">
+               <label>Color Light Bulb</label>
+               <description>Tapo Multicolor Smart Light Bulb</description>
+               <channels>
+                       <channel id="output" typeId="lightOn"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+                       <channel id="color" typeId="colorChannel"/>
+                       <channel id="colorTemperature" typeId="colorTemperature"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- LightStrip -->
+       <channel-group-type id="lightStrip">
+               <label>Color Light Strip</label>
+               <description>Tapo Multicolor Smart Light Strip</description>
+               <channels>
+                       <channel id="output" typeId="lightOn"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+                       <channel id="color" typeId="colorChannel"/>
+                       <channel id="colorTemperature" typeId="colorTemperature"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- Lightning Effect -->
+       <channel-group-type id="lightEffect">
+               <label>Lightning Effect</label>
+               <description>Tapo Lightning Effects</description>
+               <channels>
+                       <channel id="enable" typeId="effectOn"/>
+                       <channel id="brightness" typeId="dimmerChannel"/>
+                       <channel id="name" typeId="effectName"/>
+                       <channel id="custom" typeId="customEffect"/>
+                       <channel id="displayColor1" typeId="colorChannel"/>
+                       <channel id="displayColor2" typeId="colorChannel"/>
+                       <channel id="displayColor3" typeId="colorChannel"/>
+                       <channel id="displayColor4" typeId="colorChannel"/>
+               </channels>
+       </channel-group-type>
+
+
+       <!-- ############################### CHANNELS ############################### -->
+
+       <!-- ACTOR CHANNEL TYPES -->
+       <!-- OuputState Channel Type -->
+       <channel-type id="outputChannel">
+               <item-type>Switch</item-type>
+               <label>Output Switch</label>
+               <description>Switches the power state on/off</description>
+               <category>PowerOutlet</category>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- LightOn/Off Channel Type -->
+       <channel-type id="lightOn">
+               <item-type>Switch</item-type>
+               <label>Light On</label>
+               <description>Switches the light on/off</description>
+               <category>LightBulb</category>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- Dimmer Channel Type -->
+       <channel-type id="dimmerChannel">
+               <item-type>Dimmer</item-type>
+               <label>Brightness</label>
+               <description>Brightness</description>
+               <category>LightBulb</category>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- Color Channel Type -->
+       <channel-type id="colorChannel">
+               <item-type>Color</item-type>
+               <label>Color</label>
+               <description>Color</description>
+               <category>ColorLight</category>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- Color Temperature -->
+       <channel-type id="colorTemperature">
+               <item-type>Number</item-type>
+               <label>Color Temperature</label>
+               <description>This channel supports adjusting the color temperature from 2700K to 6500K.</description>
+               <category>LightBulb</category>
+               <state min="2500" max="6500" pattern="%d K"/>
+       </channel-type>
+
+
+       <!-- DEVICE-STATE CHANNEL TYPES -->
+       <!-- uptime -->
+       <channel-type id="ontime" advanced="true">
+               <item-type>Number:Time</item-type>
+               <label>On-Time</label>
+               <description>Number of seconds since the device was powered on</description>
+               <category>Time</category>
+               <state readOnly="true" pattern="%s %unit%"/>
+       </channel-type>
+
+       <!-- overheated -->
+       <channel-type id="overheated" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Device Overheated</label>
+               <description>ON if device is overheated</description>
+               <category>Alarm</category>
+               <state readOnly="true"/>
+       </channel-type>
+
+
+
+       <!-- LightningEffect Channel Type -->
+       <!-- effect on -->
+       <channel-type id="effectOn">
+               <item-type>Switch</item-type>
+               <label>Lightning Effect Enable</label>
+               <description>Switches the lightning effect on/off</description>
+               <category>LightBulb</category>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- effect name -->
+       <channel-type id="effectName">
+               <item-type>String</item-type>
+               <label>Effect Name</label>
+               <description>Name of LightningEffect</description>
+               <state readOnly="false"/>
+       </channel-type>
+
+       <!-- custom effect -->
+       <channel-type id="customEffect">
+               <item-type>Switch</item-type>
+               <label>Custom Effect</label>
+               <description>Use custom lightning effect</description>
+               <category>LightBulb</category>
+               <state readOnly="false"/>
+       </channel-type>
+
+
+       <!-- ADVANCED SETTING CHANNELS -->
+       <!-- device led -->
+       <channel-type id="led" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Switch Led</label>
+               <description>Switch the Smart Home device led on or off.</description>
+               <category>Switch</category>
+       </channel-type>
+
+       <!-- fade light -->
+       <channel-type id="fade" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Fade Light</label>
+               <description>Make the light darker or lighter slowly</description>
+               <category>Switch</category>
+       </channel-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/api/TapoUDP.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/api/TapoUDP.java
new file mode 100644 (file)
index 0000000..d48952f
--- /dev/null
@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO Smart Home device UDP-connections.
+ * THIS IS FOR TESTING
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoUDP {
+    private final Logger logger = LoggerFactory.getLogger(TapoUDP.class);
+    private static final Integer BROADCAST_TIMEOUT_MS = 5000;
+    private static final Integer BROADCAST_DISCOVERY_PORT = 20002; // int
+    private static final String BROADCAST_IP = "255.255.255.255";
+    private static final String DISCOVERY_MESSAGE_KEY = "rsa_key";
+    private static final String DISCOVERY_MESSAGE_START_BYTES = "0200000101e5110001cb8c577dd7deb8";
+    private static final Integer BUFFER_SIZE = 501;
+    private TapoCredentials credentials;
+
+    public TapoUDP(TapoCredentials credentials) {
+        this.credentials = credentials; // new TapoCredentials();
+    }
+
+    public JsonArray udpScan() {
+        try {
+            DatagramSocket udpSocket = new DatagramSocket();
+            udpSocket.setSoTimeout(BROADCAST_TIMEOUT_MS);
+            udpSocket.setBroadcast(true);
+
+            /* create payload for handshake */
+            String publicKey = credentials.getPublicKey();
+            publicKey = generateOwnRSAKey(); // credentials.getPublicKey();
+            JsonObject parameters = new JsonObject();
+            JsonObject messageObject = new JsonObject();
+            parameters.addProperty(DISCOVERY_MESSAGE_KEY, publicKey);
+            messageObject.add("params", parameters);
+
+            String discoveryMessage = messageObject.toString();
+
+            byte[] startByte = hexStringToByteArray(DISCOVERY_MESSAGE_START_BYTES);
+            byte[] message = discoveryMessage.getBytes("UTF-8");
+            byte[] sendData = new byte[startByte.length + message.length];
+            System.arraycopy(startByte, 0, sendData, 0, startByte.length);
+            System.arraycopy(message, 0, sendData, startByte.length, message.length);
+
+            DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
+                    InetAddress.getByName(BROADCAST_IP), BROADCAST_DISCOVERY_PORT);
+
+            udpSocket.send(sendPacket);
+
+            while (true) {
+                // Wait for a response
+                byte[] recvBuf = new byte[BUFFER_SIZE];
+                DatagramPacket receivePacket;
+                try {
+                    receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
+                    udpSocket.receive(receivePacket);
+                } catch (SocketTimeoutException e) {
+                    udpSocket.close();
+                    return new JsonArray();
+                } catch (Exception e) {
+                    udpSocket.close();
+                    return new JsonArray();
+                }
+
+                // Check if the message is correct
+                String responseMessage = new String(receivePacket.getData(), "UTF-8").trim();
+
+                if (responseMessage.length() == 0) {
+                    udpSocket.close();
+                }
+                String addressBC = receivePacket.getAddress().getHostAddress();
+                gotDeviceAdress(addressBC);
+            }
+        } catch (Exception e) {
+            // handle exception
+        }
+        return new JsonArray();
+    }
+
+    private void gotDeviceAdress(String ipAddress) {
+        // handle exception
+    }
+
+    private String generateOwnRSAKey() {
+        try {
+            logger.trace("generating new keypair");
+            KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
+            instance.initialize(1536, new SecureRandom());
+            KeyPair generateKeyPair = instance.generateKeyPair();
+
+            String publicKey = new String(java.util.Base64.getMimeEncoder()
+                    .encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
+            String privateKey = new String(java.util.Base64.getMimeEncoder()
+                    .encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
+            logger.trace("new privateKey: '{}'", privateKey);
+            logger.trace("new ublicKey: '{}'", publicKey);
+
+            return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
+
+        } catch (Exception e) {
+            // couldn't generate own rsa key
+            return "";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoBridgeHandler.java
new file mode 100644 (file)
index 0000000..ca35397
--- /dev/null
@@ -0,0 +1,317 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.device;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.TapoDiscoveryService;
+import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
+import org.openhab.binding.tapocontrol.internal.api.TapoUDP;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+
+/**
+ * The {@link TapoBridgeHandler} is responsible for handling commands, which are
+ * sent to one of the channels with a bridge.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBridgeHandler extends BaseBridgeHandler {
+    private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
+    private final TapoErrorHandler bridgeError = new TapoErrorHandler();
+    private final TapoBridgeConfiguration config;
+    private final HttpClient httpClient;
+    private @Nullable ScheduledFuture<?> startupJob;
+    private @Nullable ScheduledFuture<?> pollingJob;
+    private @Nullable ScheduledFuture<?> discoveryJob;
+    private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
+    private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
+    private TapoCredentials credentials;
+
+    private String uid;
+
+    public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
+        super(bridge);
+        Thing thing = getThing();
+        this.cloudConnector = new TapoCloudConnector(this, httpClient);
+        this.config = new TapoBridgeConfiguration(thing);
+        this.credentials = new TapoCredentials();
+        this.uid = thing.getUID().toString();
+        this.httpClient = httpClient;
+    }
+
+    /***********************************
+     *
+     * BRIDGE INITIALIZATION
+     *
+     ************************************/
+    @Override
+    /**
+     * INIT BRIDGE
+     * set credentials and login cloud
+     */
+    public void initialize() {
+        this.config.loadSettings();
+        this.credentials = new TapoCredentials(config.username, config.password);
+        activateBridge();
+    }
+
+    /**
+     * ACTIVATE BRIDGE
+     */
+    private void activateBridge() {
+        // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+        updateStatus(ThingStatus.UNKNOWN);
+
+        // background initialization (delay it a little bit):
+        this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
+    }
+
+    @Override
+    public void dispose() {
+        stopScheduler(this.startupJob);
+        stopScheduler(this.pollingJob);
+        stopScheduler(this.discoveryJob);
+        super.dispose();
+    }
+
+    /**
+     * ACTIVATE DISCOVERY SERVICE
+     */
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return Collections.singleton(TapoDiscoveryService.class);
+    }
+
+    /**
+     * Set DiscoveryService
+     * 
+     * @param discoveryService
+     */
+    public void setDiscoveryService(TapoDiscoveryService discoveryService) {
+        this.discoveryService = discoveryService;
+    }
+
+    /***********************************
+     *
+     * SCHEDULER
+     *
+     ************************************/
+
+    /**
+     * delayed OneTime StartupJob
+     */
+    private void delayedStartUp() {
+        loginCloud();
+        startCloudScheduler();
+        startDiscoveryScheduler();
+    }
+
+    /**
+     * Start CloudLogin Scheduler
+     */
+    protected void startCloudScheduler() {
+        Integer pollingInterval = config.cloudReconnectIntervalM;
+        if (pollingInterval > 0) {
+            logger.trace("{} starting bridge cloud sheduler", this.uid);
+
+            this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
+                    TimeUnit.MINUTES);
+        } else {
+            stopScheduler(this.pollingJob);
+        }
+    }
+
+    /**
+     * Start DeviceDiscovery Scheduler
+     */
+    protected void startDiscoveryScheduler() {
+        Integer pollingInterval = config.discoveryIntervalM;
+        if (config.cloudDiscoveryEnabled && pollingInterval > 0) {
+            logger.trace("{} starting bridge discovery sheduler", this.uid);
+
+            this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval,
+                    TimeUnit.MINUTES);
+        } else {
+            stopScheduler(this.discoveryJob);
+        }
+    }
+
+    /**
+     * Stop scheduler
+     * 
+     * @param scheduler ScheduledFeature<?> which schould be stopped
+     */
+    protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+        if (scheduler != null) {
+            scheduler.cancel(true);
+            scheduler = null;
+        }
+    }
+
+    /***********************************
+     *
+     * ERROR HANDLER
+     *
+     ************************************/
+    /**
+     * return device Error
+     * 
+     * @return
+     */
+    public TapoErrorHandler getError() {
+        return this.bridgeError;
+    }
+
+    /**
+     * set device error
+     * 
+     * @param tapoError TapoErrorHandler-Object
+     */
+    public void setError(TapoErrorHandler tapoError) {
+        this.bridgeError.set(tapoError);
+    }
+
+    /***********************************
+     *
+     * BRIDGE COMMUNICATIONS
+     *
+     ************************************/
+
+    /**
+     * Login to Cloud
+     * 
+     * @return
+     */
+    public boolean loginCloud() {
+        bridgeError.reset(); // reset ErrorHandler
+        if (!config.username.isBlank() && !config.password.isBlank()) {
+            logger.debug("{} login with user {}", this.uid, config.username);
+            if (cloudConnector.login(config.username, config.password)) {
+                updateStatus(ThingStatus.ONLINE);
+                return true;
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
+            }
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set");
+        }
+        return false;
+    }
+
+    /***********************************
+     *
+     * DEVICE DISCOVERY
+     *
+     ************************************/
+
+    /**
+     * START DEVICE DISCOVERY
+     */
+    public void discoverDevices() {
+        this.discoveryService.startScan();
+    }
+
+    /**
+     * GET DEVICELIST CONNECTED TO BRIDGE
+     * 
+     * @return devicelist
+     */
+    public JsonArray getDeviceList() {
+        JsonArray deviceList = new JsonArray();
+        if (config.cloudDiscoveryEnabled) {
+            logger.trace("{} discover devicelist from cloud", this.uid);
+            deviceList = getDeviceListCloud();
+        } else if (config.udpDiscoveryEnabled) {
+            logger.trace("{} discover devicelist from udp", this.uid);
+            deviceList = getDeviceListUDP();
+        }
+        return deviceList;
+    }
+
+    /**
+     * GET DEVICELIST FROM CLOUD
+     * returns all devices stored in cloud
+     * 
+     * @return deviceList from cloud
+     */
+    private JsonArray getDeviceListCloud() {
+        logger.trace("{} getDeviceList from cloud", this.uid);
+        bridgeError.reset(); // reset ErrorHandler
+        JsonArray deviceList = new JsonArray();
+        if (loginCloud()) {
+            deviceList = this.cloudConnector.getDeviceList();
+        }
+        return deviceList;
+    }
+
+    /**
+     * GET DEVICELIST UDP
+     * return devices discovered by UDP
+     * 
+     * @return deviceList from udp
+     */
+    public JsonArray getDeviceListUDP() {
+        bridgeError.reset(); // reset ErrorHandler
+        TapoUDP udpDiscovery = new TapoUDP(credentials);
+        return udpDiscovery.udpScan();
+    }
+
+    /***********************************
+     *
+     * BRIDGE GETTERS
+     *
+     ************************************/
+
+    public TapoCredentials getCredentials() {
+        return this.credentials;
+    }
+
+    public HttpClient getHttpClient() {
+        return this.httpClient;
+    }
+
+    public ThingUID getUID() {
+        return getThing().getUID();
+    }
+
+    public TapoBridgeConfiguration getBridgeConfig() {
+        return this.config;
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/device/TapoUniversalDevice.java
new file mode 100644 (file)
index 0000000..5f6e5a6
--- /dev/null
@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.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(DEVICE_PROPERTY_ON, command == OnOffType.ON);
+                    refreshInfo = true;
+                    break;
+                case CHANNEL_BRIGHTNESS:
+                    if (command instanceof PercentType) {
+                        Float percent = ((PercentType) command).floatValue();
+                        setBrightness(percent.intValue()); // 0..100% = 0..100
+                        refreshInfo = true;
+                    } else if (command instanceof DecimalType) {
+                        setBrightness(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR_TEMP:
+                    if (command instanceof DecimalType) {
+                        setColorTemp(((DecimalType) command).intValue());
+                        refreshInfo = true;
+                    }
+                    break;
+                case CHANNEL_COLOR:
+                    if (command instanceof HSBType) {
+                        setColor((HSBType) command);
+                        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(DEVICE_PROPERTY_ON, false);
+        } else {
+            HashMap<String, Object> newState = new HashMap<>();
+            newState.put(DEVICE_PROPERTY_ON, true);
+            newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
+            connector.sendDeviceCommands(newState);
+        }
+    }
+
+    /**
+     * SET COLOR
+     * 
+     * @param command
+     */
+    protected void setColor(HSBType command) {
+        HashMap<String, Object> newState = new HashMap<>();
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_HUE, command.getHue());
+        newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
+        newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
+        connector.sendDeviceCommands(newState);
+    }
+
+    /**
+     * SET COLORTEMP
+     * 
+     * @param colorTemp (Integer) in Kelvin
+     */
+    protected void setColorTemp(Integer colorTemp) {
+        HashMap<String, Object> newState = new HashMap<>();
+        colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
+        newState.put(DEVICE_PROPERTY_ON, true);
+        newState.put(DEVICE_PROPERTY_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
+     */
+    public void responsePasstrough(String responseBody) {
+        logger.info("({}) received response {}", uid, responseBody);
+        publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
+    }
+
+    /**
+     * UPDATE PROPERTIES
+     * 
+     * @param TapoDeviceInfo
+     */
+    @Override
+    protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
+        super.devicePropertiesChanged(deviceInfo);
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+                getPercentType(deviceInfo.getBrightness()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+                getDecimalType(deviceInfo.getColorTemp()));
+        publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
+
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+                getDecimalType(deviceInfo.getSignalLevel()));
+        publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+                getQuantityType(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
+     */
+    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/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoBridgeConfiguration.java
new file mode 100644 (file)
index 0000000..628bc51
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoBridgeConfiguration {
+    private final Logger logger = LoggerFactory.getLogger(TapoBridgeConfiguration.class);
+
+    /* THING CONFIGUTATION PROPERTYS */
+    public static final String CONFIG_EMAIL = "username";
+    public static final String CONFIG_PASS = "password";
+    public static final String CONFIG_DEVICE_IP = "ipAddress";
+    public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
+    public static final String CONFIG_CLOUD_UPDATE_INTERVAL = "cloudReconnect";
+    public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
+    public static final String CONFIG_DISCOVERY_UDP = "udpDiscovery";
+    public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
+
+    /* thing configuration parameter. */
+    public String username = "";
+    public String password = "";
+    public Boolean cloudDiscoveryEnabled = false;
+    public Boolean udpDiscoveryEnabled = false;
+    public Integer cloudReconnectIntervalM = 1440;
+    public Integer discoveryIntervalM = 30;
+
+    private Thing bridge;
+
+    /**
+     * Create settings
+     * 
+     * @param thing BridgeThing
+     */
+    public TapoBridgeConfiguration(Thing thing) {
+        this.bridge = thing;
+        loadSettings();
+    }
+
+    /**
+     * LOAD SETTINGS
+     */
+    public void loadSettings() {
+        try {
+            Configuration config = this.bridge.getConfiguration();
+            username = config.get(CONFIG_EMAIL).toString();
+            password = config.get(CONFIG_PASS).toString();
+            cloudDiscoveryEnabled = Boolean.parseBoolean(config.get(CONFIG_DISCOVERY_CLOUD).toString());
+            udpDiscoveryEnabled = Boolean.parseBoolean(config.get(CONFIG_DISCOVERY_UDP).toString());
+            cloudReconnectIntervalM = Integer.valueOf(config.get(CONFIG_CLOUD_UPDATE_INTERVAL).toString());
+            discoveryIntervalM = Integer.valueOf(config.get(CONFIG_DISCOVERY_INTERVAL).toString());
+        } catch (Exception e) {
+            logger.warn("{} error reading configuration: '{}'", bridge.getUID(), e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoDeviceInfo.java
new file mode 100644 (file)
index 0000000..89b37a3
--- /dev/null
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDeviceInfo {
+    /**
+     * AVAILABLE BUT UNUSED FIELDS
+     * remove before push to real version
+     * 
+     * private Boolean hasSetLocationInfo = false;
+     * private Integer latitude = 0;
+     * private Integer longitude = 0;
+     * private Integer timeDiff = 0;
+     * private String avatar = "";
+     * private String fwId = "";
+     * private String hwId = "";
+     * private String specs = "";
+     * private String ssid = "";
+     * private String oemId = "";
+     * private String lang = "";
+     * private String location = "";
+     */
+
+    private Boolean deviceOn = false;
+    private Boolean overheated = false;
+    private Integer brightness = 0;
+    private Integer colorTemp = 0;
+    private Integer hue = 0;
+    private Integer rssi = 0;
+    private Integer saturation = 100;
+    private Integer signalLevel = 0;
+    private Number onTime = 0;
+    private Number timeUsagePast30 = 0;
+    private Number timeUsagePast7 = 0;
+    private Number timeUsageToday = 0;
+    private String deviceId = "";
+    private String fwVer = "";
+    private String hwVer = "";
+    private String ip = "";
+    private String mac = "";
+    private String model = "";
+    private String nickname = "";
+    private String region = "";
+    private String type = "";
+    private TapoLightEffect lightEffect = new TapoLightEffect();
+
+    private JsonObject jsonObject = new JsonObject();
+
+    /**
+     * INIT
+     */
+    public TapoDeviceInfo() {
+        setData();
+    }
+
+    /**
+     * Init DeviceInfo with new Data;
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoDeviceInfo(JsonObject jso) {
+        jsonObject = jso;
+        setData();
+    }
+
+    /**
+     * Set Data (new JsonObject)
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoDeviceInfo setData(JsonObject jso) {
+        this.jsonObject = jso;
+        setData();
+        return this;
+    }
+
+    private void setData() {
+        this.brightness = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_BRIGHTNES);
+        this.colorTemp = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_COLORTEMP, BULB_MIN_COLORTEMP);
+        this.deviceId = jsonObjectToString(jsonObject, DEVICE_PROPERTY_ID);
+        this.deviceOn = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_ON);
+        this.fwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_FW);
+        this.hue = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_HUE);
+        this.hwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_HW);
+        this.ip = jsonObjectToString(jsonObject, DEVICE_PROPERTY_IP);
+        this.lightEffect = lightEffect.setData(jsonObject);
+        this.mac = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MAC);
+        this.model = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MODEL);
+        this.nickname = jsonObjectToString(jsonObject, DEVICE_PROPERTY_NICKNAME);
+        this.onTime = jsonObjectToNumber(jsonObject, DEVICE_PROPERTY_ONTIME);
+        this.overheated = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_OVERHEAT);
+        this.region = jsonObjectToString(jsonObject, DEVICE_PROPERTY_REGION);
+        this.saturation = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SATURATION);
+        this.signalLevel = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL);
+        this.rssi = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL_RSSI);
+        this.timeUsagePast7 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_7);
+        this.timeUsagePast30 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_30);
+        this.timeUsageToday = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_TODAY);
+        this.type = jsonObjectToString(jsonObject, DEVICE_PROPERTY_TYPE);
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public Integer getBrightness() {
+        return brightness;
+    }
+
+    public Integer getColorTemp() {
+        return colorTemp;
+    }
+
+    public String getFirmwareVersion() {
+        return fwVer;
+    }
+
+    public String getHardwareVersion() {
+        return hwVer;
+    }
+
+    public HSBType getHSB() {
+        DecimalType h = new DecimalType(hue);
+        PercentType s = new PercentType(saturation);
+        PercentType b = new PercentType(brightness);
+        return new HSBType(h, s, b);
+    }
+
+    public Integer getHue() {
+        return hue;
+    }
+
+    public TapoLightEffect getLightEffect() {
+        return lightEffect;
+    }
+
+    public String getIP() {
+        return ip;
+    }
+
+    public Boolean isOff() {
+        return !deviceOn;
+    }
+
+    public Boolean isOn() {
+        return deviceOn;
+    }
+
+    public Boolean isOverheated() {
+        return overheated;
+    }
+
+    public String getMAC() {
+        return formatMac(mac, MAC_DIVISION_CHAR);
+    }
+
+    public String getModel() {
+        return model.replace(" ", "_");
+    }
+
+    public String getNickname() {
+        return nickname;
+    }
+
+    public Number getOnTime() {
+        return onTime;
+    }
+
+    public String getRegion() {
+        return region;
+    }
+
+    public String getRepresentationProperty() {
+        return getMAC();
+    }
+
+    public Integer getSaturation() {
+        return saturation;
+    }
+
+    public String getSerial() {
+        return deviceId;
+    }
+
+    public Integer getSignalLevel() {
+        return signalLevel;
+    }
+
+    public Integer getRSSI() {
+        return rssi;
+    }
+
+    public Number getTimeUsagePast7() {
+        return timeUsagePast7;
+    }
+
+    public Number getTimeUsagePast30() {
+        return timeUsagePast30;
+    }
+
+    public Number getTimeUsagePastToday() {
+        return timeUsageToday;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public String toString() {
+        return jsonObject.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java b/bundles/org.openhab.binding.tapocontrol/src/test/java/org/openhab/binding/tapocontrol/internal/structures/TapoLightEffect.java
new file mode 100644 (file)
index 0000000..9832926
--- /dev/null
@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.structures;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
+
+import java.awt.Color;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Tapo-LightningEffect Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightEffect {
+    private Integer enable = 0;
+    private String id = "";
+    private String name = "";
+    private Integer custom = 0;
+    private Integer brightness = 0;
+    private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
+    private Color displayColors[] = { Color.WHITE };
+
+    private JsonObject jsonObject = new JsonObject();
+
+    /**
+     * INIT
+     */
+    public TapoLightEffect() {
+        setData();
+    }
+
+    /**
+     * Init DeviceInfo with new Data;
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoLightEffect(JsonObject jso) {
+        setData(jso);
+    }
+
+    /**
+     * Set Data (new JsonObject)
+     * 
+     * @param jso JsonObject new Data
+     */
+    public TapoLightEffect setData(JsonObject jso) {
+        /* create empty jsonObject to set efault values if has no lighning effect */
+        if (jsonObject.has(DEVICE_PROPERTY_EFFECT)) {
+            this.jsonObject = jso;
+        } else {
+            jsonObject = new JsonObject();
+        }
+        setData();
+        return this;
+    }
+
+    private void setData() {
+        this.enable = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_ENABLE);
+        this.id = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_ID);
+        this.name = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_NAME);
+        this.custom = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_CUSTOM); // jsonObjectToBool
+        this.brightness = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS);
+        // this.color_temp_range = { 9000, 9000 }; PROPERTY_LIGHNTING_ //:[9000,9000]
+        // this.displayColors[] PROPERTY_LIGHNTING_;
+    }
+
+    /***********************************
+     *
+     * SET VALUES
+     *
+     ************************************/
+
+    public void setEnable(Boolean enable) {
+        this.enable = enable ? 1 : 0;
+    }
+
+    public void setName(String value) {
+        this.name = value;
+    }
+
+    public void setCustom(Boolean enable) {
+        this.custom = enable ? 1 : 0;
+    }
+
+    public void setBrightness(Integer value) {
+        this.brightness = value;
+    }
+
+    public void setColorTempRange() {
+    }
+
+    public void setDisplayColors() {
+    }
+
+    /***********************************
+     *
+     * GET VALUES
+     *
+     ************************************/
+
+    public Integer getEnable() {
+        return this.enable;
+    }
+
+    public String getId() {
+        return this.id;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public Integer getCustom() {
+        return this.custom;
+    }
+
+    public Integer getBrightness() {
+        return this.brightness;
+    }
+
+    public Integer[] getColorTempRange() {
+        return this.colorTempRange;
+    }
+
+    public Color[] getDisplayColors() {
+        return this.displayColors;
+    }
+
+    @Override
+    public String toString() {
+        return jsonObject.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..8c516a8
--- /dev/null
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:tapo:device">
+               <parameter name="ipAddress" type="text" required="true">
+                       <context>network-address</context>
+                       <label>IP Address</label>
+               </parameter>
+               <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
+                       <label>Refresh Interval</label>
+                       <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
+                       <default>30</default>
+                       <advanced>true</advanced>
+               </parameter>
+       </config-description>
+
+       <config-description uri="bridge-type:tapo:bridge">
+               <parameter name="username" type="text" required="true">
+                       <context>email</context>
+                       <label>Username</label>
+                       <description>Tapo-Cloud Login User (e-Mail)</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <context>password</context>
+                       <label>Password</label>
+                       <description>Tapo-Cloud Login Password</description>
+               </parameter>
+               <parameter name="cloudDiscovery" type="boolean" required="false">
+                       <label>Cloud Discovery</label>
+                       <description>Use Cloud Discovery-Service</description>
+                       <default>false</default>
+                       <advanced>false</advanced>
+               </parameter>
+               <parameter name="cloudReconnect" type="integer" min="0" max="10080" required="false">
+                       <label>Cloud Reconnect Interval</label>
+                       <description>Interval for reconnecting to the Tapo-Cloud in minutes (default 1440 = 24h / 0 = disabled)</description>
+                       <default>1440</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <!--
+               <parameter name="discoveryInterval" type="integer" min="0" max="10080" required="false">
+                       <label>Background Discovery Interval</label>
+                       <description>Interval background discovery in minutes (default 60 / 0 = disabled)</description>
+                       <default>60</default>
+                       <advanced>true</advanced>
+               </parameter>
+               <parameter name="udpDiscovery" type="boolean" required="false">
+                       <label>UDP Discovery</label>
+                       <description>Use UDP Discovery-Service</description>
+                       <default>false</default>
+                       <advanced>true</advanced>
+               </parameter>
+               -->
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/thing/testdevice.xml b/bundles/org.openhab.binding.tapocontrol/src/test/resources/OH-INF/thing/testdevice.xml
new file mode 100644 (file)
index 0000000..2f68883
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- TEST-DEVICE (Universal) -->
+       <thing-type id="Test_Device">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="bridge"/>
+               </supported-bridge-type-refs>
+
+               <label>Tapo Universal TestDevice</label>
+               <description>For testing pruposes! Response is written down as info in openhab-log</description>
+               <channel-groups>
+                       <channel-group id="actuator" typeId="colorBulb"/>
+                       <channel-group id="device" typeId="deviceState"/>
+                       <channel-group id="effect" typeId="lightEffect"/>
+                       <channel-group id="debug" typeId="commandDebug"/>
+               </channel-groups>
+               <representation-property>macAddress</representation-property>
+
+               <config-description-ref uri="thing-type:tapo:device"/>
+       </thing-type>
+
+       <!-- ############################### CHANNEL-GROUPS ############################### -->
+
+       <!-- CHANNEL GROUP TYPES -->
+       <!--Device-Statuss Channel Type -->
+       <channel-group-type id="commandDebug">
+               <label>Device Communication Debug</label>
+               <description>Device resoponses and command debugging</description>
+               <channels>
+                       <channel id="deviceResponse" typeId="deviceResponse"/>
+                       <channel id="deviceCommand" typeId="deviceCommand"/>
+               </channels>
+       </channel-group-type>
+
+       <!-- ############################### CHANNELS ############################### -->
+
+       <!-- OuputState Channel Type -->
+       <channel-type id="deviceResponse">
+               <item-type>String</item-type>
+               <label>Device Response</label>
+               <description>DeviceResponse</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <!-- OuputState Channel Type -->
+       <channel-type id="deviceCommand">
+               <item-type>String</item-type>
+               <label>Device Command</label>
+               <description>command send to device. use: 'command':'value'</description>
+               <state readOnly="false"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index 9cc1c10accae58bdadc4aab4d7baca57334808e7..695088f23652327d59556e326b5b5075d30e9c38 100644 (file)
     <module>org.openhab.binding.tacmi</module>
     <module>org.openhab.binding.tado</module>
     <module>org.openhab.binding.tankerkoenig</module>
+    <module>org.openhab.binding.tapocontrol</module>
     <module>org.openhab.binding.telegram</module>
     <module>org.openhab.binding.teleinfo</module>
     <module>org.openhab.binding.tellstick</module>