/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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" }
+```
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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 "";
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
--- /dev/null
+/**
+ * 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 = "";
+ }
+}
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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()));
+ }
+}
--- /dev/null
+/**
+ * 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()));
+ }
+}
--- /dev/null
+/**
+ * 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()));
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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 "";
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
<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>