[Tapocontrol] Binding to control Tapo (by TP-Link) Devices (#11111)

* [tapocontrol] New Source Upload

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] Delete bundles/org.openhab.binding.tapocontrol directory

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] Snapshot 3.2

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] Update CODEOWNERS

Fixed bindingname

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] Update README.md

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] new "Bridge-Version"

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

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] fixed device discovery bug

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

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] Update bundles/org.openhab.binding.tapocontrol/README.md

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] code cleanup and optimization

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

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] solved review requests

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] LightStrip L900 basicly supported

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] fixed review requests

Signed-off-by: Christian Wild <christian@wild-bw.de>

* [tapocontrol] fixed compiler warnings

Signed-off-by: Christian Wild <christian@wild-bw.de>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Christian Wild 2021-11-28 15:29:21 +01:00 committed by GitHub
parent 35dbde1189
commit 612afd2e07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 6820 additions and 0 deletions

View File

@ -295,6 +295,7 @@
/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

View File

@ -1466,6 +1466,11 @@
<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>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,114 @@
# TapoControl Binding
This binding adds support to control Tapo (Copyright © TP-Link Corporation Limited) Smart Home Devices from your local openHAB system.
## Supported Things
The following Tapo-Devices are supported
### P100/P105 SmartPlug (WiFi)
* Power On/Off
* Wi-Fi signal (SignalStrength)
* On-Time (Time in seconds device is switched on)
### L510_Series dimmable SmartBulb (WiFi)
* Light On/Off
* Brightnes (Dimmer) 0-100 %
* ColorTemperature (Number) 2500-6500 K
* Wi-Fi signal (SignalStrength)
* On-Time (Time in seconds device is switched on)
### L530_Series MultiColor SmartBulb (WiFi)
* Light On/Off
* Brightnes (Dimmer) 0-100 %
* ColorTemperature (Number) 2500-6500 K
* Color (Color)
* Wi-Fi signal (SignalStrength)
* On-Time (Time in seconds device is switched on)
### L900 MultiColor LightStrip (WiFi)
* Light On/Off
* Brightnes (Dimmer) 0-100 %
* ColorTemperature (Number) 2500-6500 K
* Color (Color)
* Wi-Fi signal (SignalStrength)
* On-Time (Time in seconds device is switched on)
## Prerequisites
Before using Smart Plugs with openHAB the devices must be connected to the Wi-Fi network.
This can be done using the Tapo provided mobile app.
You need to setup a bridge (Cloud-Login) to commiunicate with your devices.
## Discovery
Discovery is done by connecting to the Tapo-Cloud Service.
All devices stored in your cloud account will be detected even if they are not in your network.
You need to know the IP-Adress of your device. This must be set manually in the thing configuration
## Bridge Configuration
The bridge needs to be configured with by `username` and `password` (Tapo-Cloud login) .
This is used for device discovery and to create a handshake (cookie) to act with your devices over the local network.
The thing has the following configuration parameters:
| Parameter | Description |
|--------------------|----------------------------------------------------------------------|
| username | Username (eMail) of your Tapo-Cloud |
| password | Password of your Tapo-Cloud |
## Thing Configuration
The thing needs to be configured with `ipAddress`.
The thing has the following configuration parameters:
| Parameter | Description |
|--------------------|----------------------------------------------------------------------|
| ipAddress | IP Address of the device. |
| pollingInterval | Refresh interval in seconds. Optional. The default is 30 seconds |
## Channels
All devices support some of the following channels:
| group | channel |type | description | things supporting this channel |
|-----------|----------------- |------------------------|------------------------------|---------------------------------|
| actuator | output | Switch | Power device on or off | P100, P105,L510, L530, L900 |
| | brightness | Dimmer | Brightness 0-100% | L510, L530, L900 |
| | colorTemperature | Number | White-Color-Temp 2500-6500K | L510, L530, L900 |
| | color | Color | Color | L530, L900 |
| device | wifiSignal | system.signal-strength | WiFi-quality-level | P100, P105, L510, L530, L900 |
| | onTime | Number:Time | seconds output is on | P100, P105, L510, L530, L900 |
## Channel Refresh
When the thing receives a `RefreshType` command the thing will send a new refreshRequest over http.
To minimize network traffic the default refresh-rate is set to 30 seconds. This can be reduced down to 10 seconds in advanced settings of the device. If any command was sent to a channel, it will do an immediately refresh of the whole device.
## Full Example
### tapocontrol.things:
```
tapocontrol:bridge:myTapoBridge "Cloud-Login" [ username="you@yourpovider.com", password="verysecret" ]
tapocontrol:P100:myTapoBridge:mySocket "My-Socket" [ ipAddress="192.168.178.150", pollingInterval=30 ]
tapocontrol:L510_Series:myTapoBridge:whiteBulb "white-light" [ ipAddress="192.168.178.151", pollingInterval=30 ]
tapocontrol:L530_Series:myTapoBridge:colorBulb "color-light" [ ipAddress="192.168.178.152", pollingInterval=30 ]
tapocontrol:L900:myTapoBridge:myLightStrip "light-strip" [ ipAddress="192.168.178.153", pollingInterval=30 ]
```
### tapocontrol.items:
```
Switch TAPO_SOCKET "socket" { channel="tapocontrol:P100:myTapoBridge:mySocket:actuator#output" }
```

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.tapocontrol</artifactId>
<name>openHAB Add-ons :: Bundles :: TapoControl Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.tapocontrol-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-tapocontrol" description="TapoControl Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.tapocontrol/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
import org.openhab.binding.tapocontrol.internal.device.TapoLightStrip;
import org.openhab.binding.tapocontrol.internal.device.TapoSmartBulb;
import org.openhab.binding.tapocontrol.internal.device.TapoSmartPlug;
import org.openhab.binding.tapocontrol.internal.device.TapoUniversalDevice;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TapoControlHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Christian Wild - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.tapocontrol")
@NonNullByDefault
public class TapoControlHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(TapoControlHandlerFactory.class);
private final Set<TapoBridgeHandler> accountHandlers = new HashSet<>();
private final HttpClient httpClient;
@Activate
public TapoControlHandlerFactory() {
// create new httpClient
httpClient = new HttpClient(new SslContextFactory.Client());
httpClient.setFollowRedirects(false);
httpClient.setMaxConnectionsPerDestination(HTTP_MAX_CONNECTIONS);
httpClient.setMaxRequestsQueuedPerDestination(HTTP_MAX_QUEUED_REQUESTS);
try {
httpClient.start();
} catch (Exception e) {
logger.error("cannot start httpClient");
}
}
@Deactivate
@Override
protected void deactivate(ComponentContext componentContext) {
super.deactivate(componentContext);
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("unable to stop httpClient");
}
}
/**
* Provides the supported thing types
*/
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
if (thingTypeUID.equals(UNIVERSAL_THING_TYPE)) {
return true;
}
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
/**
* Create handler of things.
*/
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_BRIDGE_UIDS.contains(thingTypeUID)) {
TapoBridgeHandler bridgeHandler = new TapoBridgeHandler((Bridge) thing, httpClient);
accountHandlers.add(bridgeHandler);
return bridgeHandler;
} else if (SUPPORTED_SMART_PLUG_UIDS.contains(thingTypeUID)) {
return new TapoSmartPlug(thing);
} else if (SUPPORTED_WHITE_BULB_UIDS.contains(thingTypeUID)) {
return new TapoSmartBulb(thing);
} else if (SUPPORTED_COLOR_BULB_UIDS.contains(thingTypeUID)) {
return new TapoSmartBulb(thing);
} else if (SUPPORTED_LIGHT_STRIP_UIDS.contains(thingTypeUID)) {
return new TapoLightStrip(thing);
} else if (thingTypeUID.equals(UNIVERSAL_THING_TYPE)) {
return new TapoUniversalDevice(thing);
}
return null;
}
}

View File

@ -0,0 +1,230 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* Handler class for TAPO Smart Home thing discovery
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
protected @NonNullByDefault({}) TapoBridgeHandler bridge;
/***********************************
*
* INITIALIZATION
*
************************************/
/**
* INIT CLASS
*
* @param bridgeHandler
*/
public TapoDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
}
/**
* deactivate
*/
@Override
public void activate() {
TapoBridgeConfiguration config = bridge.getBridgeConfig();
if (config.cloudDiscoveryEnabled || config.udpDiscoveryEnabled) {
startBackgroundDiscovery();
}
}
/**
* deactivate
*/
@Override
public void deactivate() {
super.deactivate();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof TapoBridgeHandler) {
TapoBridgeHandler tapoBridge = (TapoBridgeHandler) handler;
tapoBridge.setDiscoveryService(this);
this.bridge = tapoBridge;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.bridge;
}
/***********************************
*
* SCAN HANDLING
*
************************************/
/**
* Start scan manually
*/
@Override
public void startScan() {
removeOlderResults(getTimestampOfLastScan());
if (bridge != null) {
JsonArray jsonArray = bridge.getDeviceList();
handleCloudDevices(jsonArray);
}
}
/***********************************
*
* handle Results
*
************************************/
/**
* CREATE DISCOVERY RESULT
* creates discoveryResult (Thing) from JsonObject got from Cloud
*
* @param device JsonObject with device information
* @return DiscoveryResult-Object
*/
public DiscoveryResult createResult(JsonObject device) {
TapoBridgeHandler tapoBridge = this.bridge;
String deviceModel = getDeviceModel(device);
String label = getDeviceLabel(device);
String deviceMAC = device.get(CLOUD_PROPERTY_MAC).getAsString();
ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
/* create properties */
Map<String, Object> properties = new HashMap<>();
properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(deviceMAC, MAC_DIVISION_CHAR));
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.get(CLOUD_PROPERTY_FW).getAsString());
properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.get(CLOUD_PROPERTY_HW).getAsString());
properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.get(CLOUD_PROPERTY_ID).getAsString());
logger.debug("device {} discovered", deviceModel);
if (tapoBridge != null) {
ThingUID bridgeUID = tapoBridge.getUID();
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(DEVICE_REPRASENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
.build();
} else {
ThingUID thingUID = new ThingUID(BINDING_ID, deviceMAC);
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(DEVICE_REPRASENTATION_PROPERTY).withLabel(label).build();
}
}
/**
* work with result from get devices from cloud devices
*
* @param deviceList
*/
protected void handleCloudDevices(JsonArray deviceList) {
try {
for (JsonElement deviceElement : deviceList) {
if (deviceElement.isJsonObject()) {
JsonObject device = deviceElement.getAsJsonObject();
String deviceModel = getDeviceModel(device);
ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
/* create thing */
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
DiscoveryResult discoveryResult = createResult(device);
thingDiscovered(discoveryResult);
}
}
}
} catch (Exception e) {
logger.debug("error handlling CloudDevices", e);
}
}
/**
* GET DEVICEMODEL
*
* @param device JsonObject with deviceData
* @return String with DeviceModel
*/
protected String getDeviceModel(JsonObject device) {
try {
String deviceModel = device.get(CLOUD_PROPERTY_MODEL).getAsString();
deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
deviceModel = deviceModel.replace("Tapo", "");
deviceModel = deviceModel.trim();
deviceModel = deviceModel.replace(" ", "_");
return deviceModel;
} catch (Exception e) {
logger.debug("error getDeviceModel", e);
return "";
}
}
/**
* GET DEVICE LABEL
*
* @param device JsonObject with deviceData
* @return String with DeviceLabel
*/
protected String getDeviceLabel(JsonObject device) {
try {
String deviceLabel = "";
String deviceModel = getDeviceModel(device);
ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
if (SUPPORTED_SMART_PLUG_UIDS.contains(deviceUID)) {
deviceLabel = DEVICE_DESCRIPTION_SMART_PLUG;
} else if (SUPPORTED_WHITE_BULB_UIDS.contains(deviceUID)) {
deviceLabel = DEVICE_DESCRIPTION_WHITE_BULB;
} else if (SUPPORTED_COLOR_BULB_UIDS.contains(deviceUID)) {
deviceLabel = DEVICE_DESCRIPTION_COLOR_BULB;
}
return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
} catch (Exception e) {
logger.debug("error getDeviceLabel", e);
return "";
}
}
}

View File

@ -0,0 +1,238 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.api;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* Handler class for TAPO-Cloud connections.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoCloudConnector {
private final Logger logger = LoggerFactory.getLogger(TapoCloudConnector.class);
private final TapoBridgeHandler bridge;
private final Gson gson = new Gson();
private final HttpClient httpClient;
private String token = "";
private String url = TAPO_CLOUD_URL;
private String uid;
/**
* INIT CLASS
*
*/
public TapoCloudConnector(TapoBridgeHandler bridge, HttpClient httpClient) {
this.bridge = bridge;
this.httpClient = httpClient;
this.uid = bridge.getUID().getAsString();
}
/**
* handle error
*
* @param tapoError TapoErrorHandler
*/
protected void handleError(TapoErrorHandler tapoError) {
this.bridge.setError(tapoError);
}
/***********************************
*
* HTTP (Cloud)-Actions
*
************************************/
/**
* LOGIN TO CLOUD (get Token)
*
* @param username unencrypted username
* @param password unencrypted password
* @return true if login was successfull
*/
public Boolean login(String username, String password) {
this.token = getToken(username, password, TAPO_TERMINAL_UUID);
this.url = TAPO_CLOUD_URL + "?token=" + token;
return !this.token.isBlank();
}
/**
* logout
*/
public void logout() {
this.token = "";
}
/**
* GET TOKEN FROM TAPO-CLOUD
*
* @param email
* @param password
* @param terminalUUID
* @return
*/
private String getToken(String email, String password, String terminalUUID) {
String token = "";
/* create login payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = "login";
plBuilder.addParameter("appType", TAPO_APP_TYPE);
plBuilder.addParameter("cloudUserName", email);
plBuilder.addParameter("cloudPassword", password);
plBuilder.addParameter("terminalUUID", terminalUUID);
String payload = plBuilder.getPayload();
ContentResponse response = sendCloudRequest(TAPO_CLOUD_URL, payload);
if (response != null) {
token = getTokenFromResponse(response);
}
return token;
}
private String getTokenFromResponse(ContentResponse response) {
/* work with response */
if (response.getStatus() == 200) {
String rBody = response.getContentAsString();
JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class);
if (jsonObject != null) {
Integer errorCode = jsonObject.get("error_code").getAsInt();
if (errorCode == 0) {
token = jsonObject.getAsJsonObject("result").get("token").getAsString();
} else {
/* return errorcode from device */
String msg = jsonObject.get("msg").getAsString();
handleError(new TapoErrorHandler(errorCode, msg));
logger.trace("cloud returns error: '{}'", rBody);
}
} else {
handleError(new TapoErrorHandler(ERR_JSON_DECODE_FAIL));
logger.trace("unexpected json-response '{}'", rBody);
}
} else {
handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE, ERR_HTTP_RESPONSE_MSG));
logger.warn("invalid response while login");
token = "";
}
return token;
}
/**
*
* @return JsonArray with deviceList
*/
public JsonArray getDeviceList() {
/* create payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = "getDeviceList";
String payload = plBuilder.getPayload();
ContentResponse response = sendCloudRequest(this.url, payload);
if (response != null) {
return getDeviceListFromResponse(response);
}
return new JsonArray();
}
/**
* get DeviceList from Contenresponse
*
* @param response
* @return
*/
private JsonArray getDeviceListFromResponse(ContentResponse response) {
/* work with response */
if (response.getStatus() == 200) {
String rBody = response.getContentAsString();
JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class);
if (jsonObject != null) {
/* get errocode (0=success) */
Integer errorCode = jsonObject.get("error_code").getAsInt();
if (errorCode == 0) {
JsonObject result = jsonObject.getAsJsonObject("result");
return result.getAsJsonArray("deviceList");
} else {
/* return errorcode from device */
handleError(new TapoErrorHandler(errorCode, "device answers with errorcode"));
logger.trace("cloud returns error: '{}'", rBody);
}
} else {
logger.trace("enexpected json-response '{}'", rBody);
}
} else {
logger.trace("response error '{}'", response.getContentAsString());
}
return new JsonArray();
}
/***********************************
*
* HTTP-ACTIONS
*
************************************/
/**
* SEND SYNCHRON HTTP-REQUEST
*
* @param url url request is sent to
* @param payload payload (String) to send
* @return ContentResponse of request
*/
@Nullable
protected ContentResponse sendCloudRequest(String url, String payload) {
Request httpRequest = httpClient.newRequest(url).method(HttpMethod.POST.toString());
/* set header */
httpRequest.header("content-type", CONTENT_TYPE_JSON);
httpRequest.header("Accept", CONTENT_TYPE_JSON);
/* add request body */
httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
try {
ContentResponse httpResponse = httpRequest.send();
return httpResponse;
} catch (InterruptedException e) {
logger.debug("({}) sending request interrupted: {}", uid, e.toString());
handleError(new TapoErrorHandler(e));
} catch (TimeoutException e) {
logger.debug("({}) sending request timeout: {}", uid, e.toString());
handleError(new TapoErrorHandler(ERR_CONNECT_TIMEOUT, e.toString()));
} catch (Exception e) {
logger.debug("({}) sending request failed: {}", uid, e.toString());
handleError(new TapoErrorHandler(e));
}
return null;
}
}

View File

@ -0,0 +1,384 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.api;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.net.InetAddress;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* Handler class for TAPO Smart Home device connections.
* This class uses asynchronous HttpClient-Requests
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoDeviceConnector extends TapoDeviceHttpApi {
private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
private final String uid;
private final TapoDevice device;
private TapoDeviceInfo deviceInfo;
private Gson gson;
private long lastQuery = 0L;
private long lastSent = 0L;
private long lastLogin = 0L;
/**
* INIT CLASS
*
* @param config TapoControlConfiguration class
*/
public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
super(device, bridgeThingHandler);
this.device = device;
this.gson = new Gson();
this.deviceInfo = new TapoDeviceInfo();
this.uid = device.getThingUID().getAsString();
}
/***********************************
*
* LOGIN FUNCTIONS
*
************************************/
/**
* login
*
* @return true if success
*/
public boolean login() {
if (this.pingDevice()) {
logger.trace("({}) sending login to url '{}'", uid, deviceURL);
long now = System.currentTimeMillis();
if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
this.lastLogin = now;
unsetToken();
unsetCookie();
/* create ssl-handschake (cookie) */
String cookie = createHandshake();
if (!cookie.isBlank()) {
setCookie(cookie);
String token = queryToken();
setToken(token);
}
} else {
logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
}
return this.loggedIn();
} else {
logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE, "no ping while login"));
return false;
}
}
/***********************************
*
* DEVICE ACTIONS
*
************************************/
/**
* send custom command to device
*
* @param plBuilder Payloadbuilder with unencrypted payload
*/
public void sendCustomQuery(String queryMethod) {
/* create payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = queryMethod;
sendCustomPayload(plBuilder);
}
/**
* send custom command to device
*
* @param plBuilder Payloadbuilder with unencrypted payload
*/
public void sendCustomPayload(PayloadBuilder plBuilder) {
long now = System.currentTimeMillis();
if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
String payload = plBuilder.getPayload();
sendSecurePasstrhroug(payload, DEVICE_CMD_CUSTOM);
} else {
logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
}
}
/**
* send "set_device_info" command to device
*
* @param name Name of command to send
* @param value Value to send to control
*/
public void sendDeviceCommand(String name, Object value) {
long now = System.currentTimeMillis();
if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
this.lastSent = now;
/* create payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = DEVICE_CMD_SETINFO;
plBuilder.addParameter(name, value);
String payload = plBuilder.getPayload();
sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
} else {
logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
}
}
/**
* send multiple "set_device_info" commands to device
*
* @param map HashMap<String, Object> (name, value of parameter)
*/
public void sendDeviceCommands(HashMap<String, Object> map) {
long now = System.currentTimeMillis();
if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
this.lastSent = now;
/* create payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = DEVICE_CMD_SETINFO;
for (HashMap.Entry<String, Object> entry : map.entrySet()) {
plBuilder.addParameter(entry.getKey(), entry.getValue());
}
String payload = plBuilder.getPayload();
sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
} else {
logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
}
}
/**
* Query Info from Device adn refresh deviceInfo
*/
public void queryInfo() {
queryInfo(false);
}
/**
* Query Info from Device adn refresh deviceInfo
*
* @param ignoreGap ignore gap to last query. query anyway
*/
public void queryInfo(boolean ignoreGap) {
logger.trace("({}) DeviceConnetor_queryInfo from '{}'", uid, deviceURL);
long now = System.currentTimeMillis();
if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
this.lastQuery = now;
/* create payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = DEVICE_CMD_GETINFO;
String payload = plBuilder.getPayload();
sendSecurePasstrhroug(payload, DEVICE_CMD_GETINFO);
} else {
logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastQuery);
}
}
/**
* SEND SECUREPASSTHROUGH
* encprypt payload and send to device
*
* @param payload payload sent to device
* @param command command executed - this will handle result
*/
protected void sendSecurePasstrhroug(String payload, String command) {
/* encrypt payload */
String encryptedPayload = encryptPayload(payload);
/* create secured payload */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = "securePassthrough";
plBuilder.addParameter("request", encryptedPayload);
String securePassthroughPayload = plBuilder.getPayload();
sendAsyncRequest(deviceURL, securePassthroughPayload, command);
}
/***********************************
*
* HANDLE RESPONSES
*
************************************/
/**
* Handle SuccessResponse (setDeviceInfo)
*
* @param responseBody String with responseBody from device
*/
@Override
protected void handleSuccessResponse(String responseBody) {
JsonObject jsnResult = getJsonFromResponse(responseBody);
Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_JSON_DECODE_FAIL);
if (errorCode != 0) {
logger.debug("({}) set deviceInfo not succesfull: {}", uid, jsnResult);
this.device.handleConnectionState();
}
this.device.responsePasstrough(responseBody);
}
/**
* handle JsonResponse (getDeviceInfo)
*
* @param responseBody String with responseBody from device
*/
@Override
protected void handleDeviceResult(String responseBody) {
JsonObject jsnResult = getJsonFromResponse(responseBody);
if (jsnResult.has("device_id")) {
this.deviceInfo = new TapoDeviceInfo(jsnResult);
this.device.setDeviceInfo(deviceInfo);
} else {
this.deviceInfo = new TapoDeviceInfo();
this.device.handleConnectionState();
}
this.device.responsePasstrough(responseBody);
}
/**
* handle custom response
*
* @param responseBody String with responseBody from device
*/
@Override
protected void handleCustomResponse(String responseBody) {
this.device.responsePasstrough(responseBody);
}
/**
* handle error
*
* @param te TapoErrorHandler
*/
@Override
protected void handleError(TapoErrorHandler tapoError) {
this.device.setError(tapoError);
}
/**
* get Json from response
*
* @param responseBody
* @return JsonObject with result
*/
private JsonObject getJsonFromResponse(String responseBody) {
JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class);
/* get errocode (0=success) */
if (jsonObject != null) {
Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
if (errorCode == 0) {
/* decrypt response */
jsonObject = gson.fromJson(responseBody, JsonObject.class);
logger.trace("({}) received result: {}", uid, responseBody);
if (jsonObject != null) {
/* return result if set / else request was successfull */
if (jsonObject.has("result")) {
return jsonObject.getAsJsonObject("result");
} else {
return jsonObject;
}
}
} else {
/* return errorcode from device */
TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
handleError(te);
return jsonObject;
}
}
logger.debug("({}) sendPayload exception {}", uid, responseBody);
handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE));
return new JsonObject();
}
/***********************************
*
* GET RESULTS
*
************************************/
/**
* Check if device is online
*
* @return true if device is online
*/
public Boolean isOnline() {
return isOnline(false);
}
/**
* Check if device is online
*
* @param raiseError if true
* @return true if device is online
*/
public Boolean isOnline(Boolean raiseError) {
if (pingDevice()) {
return true;
} else {
logger.trace("({}) device is offline (no ping)", uid);
if (raiseError) {
handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE));
}
logout();
return false;
}
}
/**
* IP-Adress
*
* @return String ipAdress
*/
public String getIP() {
return this.ipAddress;
}
/**
* PING IP Adress
*
* @return true if ping successfull
*/
public Boolean pingDevice() {
try {
InetAddress address = InetAddress.getByName(this.ipAddress);
return address.isReachable(TAPO_PING_TIMEOUT_MS);
} catch (Exception e) {
logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
return false;
}
}
}

View File

@ -0,0 +1,564 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.api;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
import org.openhab.binding.tapocontrol.internal.helpers.TapoCipher;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* Handler class for TAPO Smart Home device connections.
* This class uses synchronous HttpClient-Requests for login to device
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoDeviceHttpApi {
private final Logger logger = LoggerFactory.getLogger(TapoDeviceHttpApi.class);
private final String uid;
private final TapoCipher tapoCipher;
private final TapoBridgeHandler bridge;
private Gson gson;
private String token = "";
private String cookie = "";
protected String deviceURL = "";
protected String ipAddress = "";
/**
* INIT CLASS
*
* @param config TapoControlConfiguration class
*/
public TapoDeviceHttpApi(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
this.bridge = bridgeThingHandler;
this.tapoCipher = new TapoCipher();
this.gson = new Gson();
this.uid = device.getThingUID().getAsString();
String ipAddress = device.getIpAddress();
setDeviceURL(ipAddress);
}
/***********************************
*
* DELEGATING FUNCTIONS
* will normaly be delegated to extension-classes(TapoDeviceConnector)
*
************************************/
/**
* handle SuccessResponse (setDeviceInfo)
*
* @param responseBody String with responseBody from device
*/
protected void handleSuccessResponse(String responseBody) {
}
/**
* handle JsonResponse (getDeviceInfo)
*
* @param responseBody String with responseBody from device
*/
protected void handleDeviceResult(String responseBody) {
}
/**
* handle custom response
*
* @param responseBody String with responseBody from device
*/
protected void handleCustomResponse(String responseBody) {
}
/**
* handle error
*
* @param te TapoErrorHandler
*/
protected void handleError(TapoErrorHandler tapoError) {
}
/***********************************
*
* LOGIN FUNCTIONS
*
************************************/
/**
* Create Handshake and set cookie
*
* @return true if handshake (cookie) was created
*/
protected String createHandshake() {
String cookie = "";
try {
/* create payload for handshake */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = "handshake";
plBuilder.addParameter("key", bridge.getCredentials().getPublicKey()); // ?.decode("UTF-8")
String payload = plBuilder.getPayload();
/* send request (create ) */
logger.trace("({}) create handhsake with payload: {}", uid, payload.toString());
ContentResponse response = sendRequest(this.deviceURL, payload);
if (response != null && getErrorCode(response) == 0) {
String encryptedKey = getKeyFromResponse(response);
this.tapoCipher.setKey(encryptedKey, bridge.getCredentials());
cookie = getCookieFromResponse(response);
}
} catch (Exception e) {
logger.debug("({}) could not createHandshake: {}", uid, e.toString());
handleError(new TapoErrorHandler(ERR_HAND_SHAKE_FAILED, "could not createHandshake"));
}
return cookie;
}
/**
* return encrypted key from 'handshake' request
*
* @param response ContentResponse from "handshake" method
* @return
*/
private String getKeyFromResponse(ContentResponse response) {
String rBody = response.getContentAsString();
JsonObject jsonObj = gson.fromJson(rBody, JsonObject.class);
if (jsonObj != null) {
logger.trace("({}) received awnser: {}", uid, rBody);
return jsonObjectToString(jsonObj.getAsJsonObject("result"), "key");
} else {
logger.warn("({}) could not getKeyFromResponse '{}'", uid, rBody);
handleError(new TapoErrorHandler(ERR_HAND_SHAKE_FAILED, "could not getKeyFromResponse"));
}
return "";
}
/**
* return cookie from 'handshake' request
*
* @param response ContentResponse from "handshake" metho
* @return
*/
private String getCookieFromResponse(ContentResponse response) {
String cookie = "";
try {
cookie = response.getHeaders().get("Set-Cookie").split(";")[0];
logger.trace("({}) got cookie: '{}'", uid, cookie);
} catch (Exception e) {
logger.warn("({}) could not getCookieFromResponse", uid);
handleError(new TapoErrorHandler(ERR_HAND_SHAKE_FAILED, "could not getCookieFromResponse"));
}
return cookie;
}
/**
* Query Token from device
*
* @return String with token returned from device
*/
protected String queryToken() {
String token = "";
try {
/* encrypt login credentials */
PayloadBuilder plBuilder = new PayloadBuilder();
plBuilder.method = "login_device";
plBuilder.addParameter("username", bridge.getCredentials().getEncodedEmail());
plBuilder.addParameter("password", bridge.getCredentials().getEncodedPassword());
String payload = plBuilder.getPayload();
String encryptedPayload = this.encryptPayload(payload);
/* create secured login informations */
plBuilder = new PayloadBuilder();
plBuilder.method = "securePassthrough";
plBuilder.addParameter("request", encryptedPayload);
String securePassthroughPayload = plBuilder.getPayload();
/* sendRequest and get Token */
ContentResponse response = sendRequest(deviceURL, securePassthroughPayload);
token = getTokenFromResponse(response);
} catch (Exception e) {
logger.debug("({}) error building login payload: {}", uid, e.toString());
handleError(new TapoErrorHandler(e, "error building login payload"));
}
return token;
}
/**
* get Token from "login"-request
*
* @param response
* @return
*/
private String getTokenFromResponse(@Nullable ContentResponse response) {
String result = "";
TapoErrorHandler tapoError = new TapoErrorHandler();
if (response != null && response.getStatus() == 200) {
String rBody = response.getContentAsString();
String decryptedResponse = this.decryptResponse(rBody);
logger.trace("({}) received result: {}", uid, decryptedResponse);
/* get errocode (0=success) */
JsonObject jsonObject = gson.fromJson(decryptedResponse, JsonObject.class);
if (jsonObject != null) {
Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_JSON_DECODE_FAIL);
if (errorCode == 0) {
/* return result if set / else request was successfull */
result = jsonObjectToString(jsonObject.getAsJsonObject("result"), "token");
} else {
/* return errorcode from device */
tapoError.raiseError(errorCode, "could not get token");
logger.debug("({}) login recieved errorCode {} - {}", uid, errorCode, tapoError.getMessage());
}
} else {
logger.debug("({}) unexpected json-response '{}'", uid, decryptedResponse);
tapoError.raiseError(ERR_JSON_ENCODE_FAIL, "could not get token");
}
} else {
logger.debug("({}) invalid response while login", uid);
tapoError.raiseError(ERR_HTTP_RESPONSE, "invalid response while login");
}
/* handle error */
if (tapoError.hasError()) {
handleError(tapoError);
}
return result;
}
/***********************************
*
* HTTP-ACTIONS
*
************************************/
/**
* SEND SYNCHRON HTTP-REQUEST
*
* @param url url request is sent to
* @param payload payload (String) to send
* @return ContentResponse of request
*/
@Nullable
protected ContentResponse sendRequest(String url, String payload) {
logger.trace("({}) sendRequest to '{}' with cookie '{}'", uid, url, this.cookie);
Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
/* set header */
httpRequest = setHeaders(httpRequest);
httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
/* add request body */
httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
try {
ContentResponse httpResponse = httpRequest.send();
return httpResponse;
} catch (InterruptedException e) {
logger.debug("({}) sending request interrupted: {}", uid, e.toString());
handleError(new TapoErrorHandler(e));
} catch (TimeoutException e) {
logger.debug("({}) sending request timeout: {}", uid, e.toString());
handleError(new TapoErrorHandler(ERR_CONNECT_TIMEOUT, e.toString()));
} catch (Exception e) {
logger.debug("({}) sending request failed: {}", uid, e.toString());
handleError(new TapoErrorHandler(e));
}
return null;
}
/**
* SEND ASYNCHRONOUS HTTP-REQUEST
* (don't wait for awnser with programm code)
*
* @param url string url request is sent to
* @param payload data-payload
* @param command command executed - this will handle RepsonseType
*/
protected void sendAsyncRequest(String url, String payload, String command) {
logger.trace("({}) sendAsncRequest to '{}' with cookie '{}'", uid, url, this.cookie);
try {
Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
/* set header */
httpRequest = setHeaders(httpRequest);
/* add request body */
httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(Result result) {
final HttpResponse response = (HttpResponse) result.getResponse();
if (result.getFailure() != null) {
/* handle result errors */
Throwable e = result.getFailure();
String errorMessage = getValueOrDefault(e.getMessage(), "");
if (e instanceof TimeoutException) {
logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
handleError(new TapoErrorHandler(ERR_CONNECT_TIMEOUT, errorMessage));
} else {
logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
handleError(new TapoErrorHandler(new Exception(e), errorMessage));
}
} else if (response.getStatus() != 200) {
logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE, getContentAsString()));
} else {
/* request succesfull */
String rBody = getContentAsString();
rBody = decryptResponse(rBody);
logger.trace("({}) requestCompleted '{}'", uid, rBody);
/* handle result */
switch (command) {
case DEVICE_CMD_SETINFO:
handleSuccessResponse(rBody);
break;
case DEVICE_CMD_GETINFO:
handleDeviceResult(rBody);
break;
case DEVICE_CMD_CUSTOM:
handleCustomResponse(rBody);
break;
}
}
}
});
} catch (Exception e) {
handleError(new TapoErrorHandler(e));
}
}
/**
* return error code from response
*
* @param response
* @return 0 if request was successfull
*/
protected Integer getErrorCode(@Nullable ContentResponse response) {
try {
if (response != null) {
String responseBody = response.getContentAsString();
return getErrorCode(responseBody);
} else {
return ERR_HTTP_RESPONSE;
}
} catch (Exception e) {
return ERR_HTTP_RESPONSE;
}
}
/**
* return error code from responseBody
*
* @param responseBody
* @return 0 if request was successfull
*/
protected Integer getErrorCode(String responseBody) {
try {
JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class);
/* get errocode (0=success) */
Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_JSON_DECODE_FAIL);
if (errorCode == 0) {
return 0;
} else {
logger.debug("({}) device returns errorcode '{}'", uid, errorCode);
handleError(new TapoErrorHandler(errorCode));
return errorCode;
}
} catch (Exception e) {
return ERR_HTTP_RESPONSE;
}
}
/**
* SET HTTP-HEADERS
*/
private Request setHeaders(Request httpRequest) {
/* set header */
httpRequest.header("content-type", CONTENT_TYPE_JSON);
httpRequest.header("Accept", CONTENT_TYPE_JSON);
if (!this.cookie.isEmpty()) {
httpRequest.header(HTTP_AUTH_TYPE_COOKIE, this.cookie);
}
return httpRequest;
}
/***********************************
*
* ENCRYPTION / CODING
*
************************************/
/**
* Decrypt Response
*
* @param responseBody encrypted string from response-body
* @return String decrypted responseBody
*/
protected String decryptResponse(String responseBody) {
try {
JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class);
if (jsonObject != null) {
String encryptedResponse = jsonObjectToString(jsonObject.getAsJsonObject("result"), "response");
return tapoCipher.decode(encryptedResponse);
} else {
handleError(new TapoErrorHandler(ERR_JSON_DECODE_FAIL));
}
} catch (Exception ex) {
logger.debug("({}) exception '{}' decryptingResponse: '{}'", uid, ex.toString(), responseBody);
}
return responseBody;
}
/**
* encrypt payload
*
* @param payload
* @return encrypted payload
*/
protected String encryptPayload(String payload) {
try {
return tapoCipher.encode(payload);
} catch (Exception ex) {
logger.debug("({}) exception encoding Payload '{}'", uid, ex.toString());
return "";
}
}
/**
* perform logout (dispose cookie)
*/
public void logout() {
logger.trace("DeviceHttpApi_logout");
unsetToken();
unsetCookie();
}
/***********************************
*
* GET RESULTS
*
************************************/
/**
* Logged In
*
* @return true if logged in
*/
public Boolean loggedIn() {
return loggedIn(false);
}
/**
* Logged In
*
* @param raiseError if true
* @return true if logged in
*/
public Boolean loggedIn(Boolean raiseError) {
if (!this.token.isBlank() && !this.cookie.isBlank()) {
return true;
} else {
logger.trace("({}) not logged in", uid);
if (raiseError) {
handleError(new TapoErrorHandler(ERR_LOGIN));
}
return false;
}
}
/***********************************
*
* SET VALUES
*
************************************/
/**
* Set new ipAddress
*
* @param new ipAdress
*/
public void setDeviceURL(String ipAddress) {
this.ipAddress = ipAddress;
this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
}
/**
* Set new ipAdresss with token
*
* @param ipAddress ipAddres of device
* @param token token from login-ressult
*/
public void setDeviceURL(String ipAddress, String token) {
this.ipAddress = ipAddress;
this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
}
/**
* Set new token
*
* @param deviceURL
* @param token
*/
protected void setToken(String token) {
if (!token.isBlank()) {
String url = this.deviceURL.replaceAll("\\?token=\\w*", "");
this.deviceURL = url + "?token=" + token;
}
this.token = token;
}
/**
* Unset Token (device logout)
*/
protected void unsetToken() {
this.deviceURL = this.deviceURL.replaceAll("\\?token=\\w*", "");
this.token = "";
}
/**
* Set new cookie
*
* @param cookie
*/
protected void setCookie(String cookie) {
this.cookie = cookie;
}
/**
* Unset Cookie (device logout)
*/
protected void unsetCookie() {
bridge.getHttpClient().getCookieStore().removeAll();
this.cookie = "";
}
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.constants;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TapoBindingSettings} class defines common constants, which are
* used across the whole binding.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoBindingSettings {
public static final String BINDING_ID = "tapocontrol";
// List of all constant configurations
public static final String HTTP_HEADER_AUTH = "Authorization";
public static final String HTTP_AUTH_TYPE_BASIC = "Basic";
public static final String HTTP_AUTH_TYPE_COOKIE = "cookie";
public static final String CONTENT_CHARSET = "UTF-8";
public static final String CONTENT_TYPE_JSON = "application/json";
public static final String TAPO_CLOUD_URL = "https://eu-wap.tplinkcloud.com";
public static final String TAPO_APP_TYPE = "Tapo_Ios";
public static final String TAPO_TERMINAL_UUID = "0A950402-7224-46EB-A450-7362CDB902A2";
public static final String TAPO_DEVICE_URL = "http://%s/app";
public static final Integer HTTP_MAX_CONNECTIONS = 10; // setMaxConnectionsPerDestination for HTTP-Client
public static final Integer HTTP_MAX_QUEUED_REQUESTS = 10; // setMaxRequestsQueuedPerDestination for HTTP-Client
public static final Integer TAPO_HTTP_TIMEOUT_MS = 5000; // http request timeout
public static final Integer TAPO_PING_TIMEOUT_MS = 2000; // ping timeout
public static final Integer TAPO_REFRESH_MIN_GAP_MS = 5000; // min gap between sending refresh request
public static final Integer TAPO_SEND_MIN_GAP_MS = 1000; // min gap between sending command request
public static final Integer TAPO_LOGIN_MIN_GAP_MS = 5000; // min gap between sending login request
public static final Integer TAPO_LOGIN_MAX_GAP_M = 1440; // max minutes to relogin to device
public static final Integer TAPO_DISCOVERY_TIMEOUT_S = 6; // timout device discovery in seconds
public static final Integer POLLING_MIN_INTERVAL_S = 10; // min polling interval (settings)
// FORMATING CONSTANTS
public static final String IPV4_REGEX = "(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))";
public static final char MAC_DIVISION_CHAR = '-';
// LIST OF DEVICE-COMMANDS
public static final String DEVICE_CMD_GETINFO = "get_device_info";
public static final String DEVICE_CMD_SETINFO = "set_device_info";
public static final String DEVICE_CMD_CUSTOM = "custom_command";
}

View File

@ -0,0 +1,157 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.constants;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TapoErrorConstants} class defines error-message constants
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoErrorConstants {
/****************************************
* LIST OF ERROR CODES
****************************************/
// List of API-ErrorCodes
public static final Integer ERR_COMMON_FAILED = -1;
public static final Integer ERR_SESSION_TIMEOUT = 9999;
public static final Integer ERR_NULL_TRANSPORT = 1000;
public static final Integer ERR_REQUEST = 1002;
public static final Integer ERR_HAND_SHAKE_FAILED = 1100;
public static final Integer ERR_LOGIN_FAILED = 1111;
public static final Integer ERR_HTTP_TRANSPORT_FAILED = 1112;
public static final Integer ERR_MULTI_REQUEST_FAILED = 1200;
public static final Integer ERR_JSON_DECODE_FAIL = -1003;
public static final Integer ERR_JSON_ENCODE_FAIL = -1004;
public static final Integer ERR_AES_DECODE_FAIL = -1005;
public static final Integer ERR_REQUEST_LEN_ERROR = -1006;
public static final Integer ERR_CLOUD_FAILED = -1007;
public static final Integer ERR_PARAMS = -1008;
public static final Integer ERR_RSA_KEY_LENGTH = -1010;
public static final Integer ERR_SESSION_PARAM = -1101;
public static final Integer ERR_QUICK_SETUP = -1201;
public static final Integer ERR_DEVICE = -1301;
public static final Integer ERR_DEVICE_NEXT_EVENT = -1302;
public static final Integer ERR_FIRMWARE = -1401;
public static final Integer ERR_FIRMWARE_VER_ERROR = -1402;
public static final Integer ERR_LOGIN = -1501;
public static final Integer ERR_TIME = -1601;
public static final Integer ERR_TIME_SYS = -1602;
public static final Integer ERR_TIME_SAVE = -1603;
public static final Integer ERR_WIRELESS = -1701;
public static final Integer ERR_WIRELESS_UNSUPPORTED = -1702;
public static final Integer ERR_SCHEDULE = -1801;
public static final Integer ERR_SCHEDULE_FULL = -1802;
public static final Integer ERR_SCHEDULE_CONFLICT = -1803;
public static final Integer ERR_SCHEDULE_SAVE = -1804;
public static final Integer ERR_SCHEDULE_INDEX = -1805;
public static final Integer ERR_COUNTDOWN = -1901;
public static final Integer ERR_COUNTDOWN_CONFLICT = -1902;
public static final Integer ERR_COUNTDOWN_SAVE = -1903;
public static final Integer ERR_ANTITHEFT = -2001;
public static final Integer ERR_ANTITHEFT_CONFLICT = -2002;
public static final Integer ERR_ANTITHEFT_SAVE = -2003;
public static final Integer ERR_ACCOUNT = -2101;
public static final Integer ERR_STAT = -2201;
public static final Integer ERR_STAT_SAVE = -2202;
public static final Integer ERR_DST = -2301;
public static final Integer ERR_DST_SAVE = -2302;
// -20661
// List of Binding-ErrorCodes
public static final Integer ERR_HTTP_RESPONSE = 9001;
public static final Integer ERR_COOKIE = 9002;
public static final Integer ERR_CREDENTIALS = 9003;
public static final Integer ERR_DEVICE_OFFLINE = 9009;
public static final Integer ERR_CONNECT_TIMEOUT = 9010;
// List of Config-ErrorCodes
public static final Integer ERR_CONF_IP = 10001; // ip not set
public static final Integer ERR_CONF_CREDENTIALS = 10002; // credentials not set
public static final Integer ERR_NO_BRIDGE = 10003; // no bridge configured
/****************************************
* LIST OF ERROR MESSAGES
****************************************/
// List of CLOUD-Error-Messages
public static final String ERR_COMMON_FAILED_MSG = ""; // -1;
public static final String ERR_SESSION_TIMEOUT_MSG = "Session Timeout"; // 9999;
public static final String ERR_NULL_TRANSPORT_MSG = ""; // 1000;
public static final String ERR_REQUEST_MSG = "Invalid request or command"; // 1002;
public static final String ERR_HAND_SHAKE_FAILED_MSG = "Can't create handshake"; // 1100;
public static final String ERR_LOGIN_FAILED_MSG = ""; // 1111;
public static final String ERR_HTTP_TRANSPORT_FAILED_MSG = ""; // 1112;
public static final String ERR_MULTI_REQUEST_FAILED_MSG = ""; // 1200;
public static final String ERR_JSON_DECODE_FAIL_MSG = "json decode failed"; // -1003;
public static final String ERR_JSON_ENCODE_FAIL_MSG = "json encode failed"; // -1004;
public static final String ERR_AES_DECODE_FAIL_MSG = ""; // -1005;
public static final String ERR_REQUEST_LEN_ERROR_MSG = ""; // -1006;
public static final String ERR_CLOUD_FAILED_MSG = ""; // -1007;
public static final String ERR_PARAMS_MSG = "received invalid parameter"; // -1008;
public static final String ERR_RSA_KEY_LENGTH_MSG = "Invalid Public Key Length"; // -1010;
public static final String ERR_SESSION_PARAM_MSG = ""; // -1101;
public static final String ERR_QUICK_SETUP_MSG = ""; // -1201;
public static final String ERR_DEVICE_MSG = ""; // -1301;
public static final String ERR_DEVICE_NEXT_EVENT_MSG = ""; // -1302;
public static final String ERR_FIRMWARE_MSG = ""; // -1401;
public static final String ERR_FIRMWARE_VER_ERROR_MSG = ""; // -1402;
public static final String ERR_LOGIN_MSG = "Login Error"; // -1501;
public static final String ERR_TIME_MSG = ""; // -1601;
public static final String ERR_TIME_SYS_MSG = ""; // -1602;
public static final String ERR_TIME_SAVE_MSG = ""; // -1603;
public static final String ERR_WIRELESS_MSG = ""; // -1701;
public static final String ERR_WIRELESS_UNSUPPORTED_MSG = ""; // -1702;
public static final String ERR_SCHEDULE_MSG = ""; // -1801;
public static final String ERR_SCHEDULE_FULL_MSG = ""; // -1802;
public static final String ERR_SCHEDULE_CONFLICT_MSG = ""; // -1803;
public static final String ERR_SCHEDULE_SAVE_MSG = ""; // -1804;
public static final String ERR_SCHEDULE_INDEX_MSG = ""; // -1805;
public static final String ERR_COUNTDOWN_MSG = ""; // -1901;
public static final String ERR_COUNTDOWN_CONFLICT_MSG = ""; // -1902;
public static final String ERR_COUNTDOWN_SAVE_MSG = ""; // -1903;
public static final String ERR_ANTITHEFT_MSG = ""; // -2001;
public static final String ERR_ANTITHEFT_CONFLICT_MSG = ""; // -2002;
public static final String ERR_ANTITHEFT_SAVE_MSG = ""; // -2003;
public static final String ERR_ACCOUNT_MSG = ""; // -2101;
public static final String ERR_STAT_MSG = ""; // -2201;
public static final String ERR_STAT_SAVE_MSG = ""; // -2202;
public static final String ERR_DST_MSG = ""; // -2301;
public static final String ERR_DST_SAVE_MSG = ""; // -2302;
// List of Binding-Error-Messages
public static final String ERR_HTTP_RESPONSE_MSG = "Invalid HTTP-Response"; // 9001
public static final String ERR_COOKIE_MSG = "Cookie Error"; // 9002
public static final String ERR_DEVICE_OFFLINE_MSG = "Device Offline"; // 9009
public static final String ERR_CREDENTIALS_MSG = "Invalid Request or Credentials";
public static final String ERR_CONNECT_TIMEOUT_MSG = "Connection Timeout - device not reachable";
// List of Config-Error-Messages
public static final String ERR_CONF_IP_MSG = "IP-Address not valid"; // 10001;
public static final String ERR_CONF_CREDENTIALS_MSG = "credentials not set (bridge)"; // 10002;
public static final String ERR_NO_BRIDGE_MSG = "no brigde configured"; // 10003;
/****************************************
* ErrorTypes
****************************************/
// communication errors - set device to offline (retry connect)
public static final Set<Integer> LIST_COMMUNICATION_ERRORS = Set.of(ERR_HTTP_RESPONSE, ERR_COOKIE,
ERR_DEVICE_OFFLINE, ERR_CONNECT_TIMEOUT);
// configuration errors - set device to state configuration error (don't retry)
public static final Set<Integer> LIST_CONFIGURATION_ERRORS = Set.of(ERR_CREDENTIALS);
// reauthenticate errors (trying login immediatly)
public static final Set<Integer> LIST_REAUTH_ERRORS = Set.of(ERR_SESSION_TIMEOUT, ERR_HAND_SHAKE_FAILED);
}

View File

@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
***/
package org.openhab.binding.tapocontrol.internal.constants;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link TapoBindingSettings} class defines common constants, which are
* used across the whole binding.
*
* @author Christian Wild - Initial contribution
***/
@NonNullByDefault
public class TapoThingConstants {
public static final String DEVICE_VENDOR = "Tapo";
/*** LIST OF SUPPORTED DEVICE NAMES ***/
public static final String DEVICE_BRIDGE = "bridge";
public static final String DEVICE_P100 = "P100";
public static final String DEVICE_P105 = "P105";
public static final String DEVICE_L510E = "L510_Series";
public static final String DEVICE_L530E = "L530_Series";
public static final String DEVICE_L900 = "L900";
public static final String DEVICE_UNIVERSAL = "Test_Device";
/*** LIST OF SUPPORTED DEVICE DESCRIPTIONS ***/
public static final String DEVICE_DESCRIPTION_BRIDGE = "TapoControl Cloud-Login";
public static final String DEVICE_DESCRIPTION_SMART_PLUG = "SmartPlug";
public static final String DEVICE_DESCRIPTION_WHITE_BULB = "White-Light-Bulb";
public static final String DEVICE_DESCRIPTION_COLOR_BULB = "Color-Light-Bulb";
public static final String DEVICE_DESCRIPTION_LIGHTSTRIP = "LightStrip";
/*** LIST OF SUPPORTED THING UIDS ***/
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_BRIDGE);
public static final ThingTypeUID P100_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P100);
public static final ThingTypeUID P105_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P105);
public static final ThingTypeUID L510E_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L510E);
public static final ThingTypeUID L530E_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L530E);
public static final ThingTypeUID L900_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L900);
public static final ThingTypeUID UNIVERSAL_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_UNIVERSAL);
/*** SET OF SUPPORTED UIDS ***/
public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_UIDS = Set.of(BRIDGE_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_SMART_PLUG_UIDS = Set.of(P100_THING_TYPE, P105_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_WHITE_BULB_UIDS = Set.of(L510E_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_COLOR_BULB_UIDS = Set.of(L530E_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_LIGHT_STRIP_UIDS = Set.of(L900_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream
.of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_SMART_PLUG_UIDS, SUPPORTED_WHITE_BULB_UIDS,
SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS)
.flatMap(Set::stream).collect(Collectors.toSet()));
/*** THINGS WITH CHANNEL GROUPS ***/
public static final Set<ThingTypeUID> CHANNEL_GROUP_THING_SET = Collections
.unmodifiableSet(Stream
.of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_SMART_PLUG_UIDS, SUPPORTED_WHITE_BULB_UIDS,
SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS)
.flatMap(Set::stream).collect(Collectors.toSet()));
/*** DEVICE PROPERTY STRINGS (CLOUD) ***/
public static final String CLOUD_PROPERTY_ALIAS = "alias";
public static final String CLOUD_PROPERTY_FW = "fwVer";
public static final String CLOUD_PROPERTY_HW = "deviceHwVer";
public static final String CLOUD_PROPERTY_ID = "deviceId";
public static final String CLOUD_PROPERTY_MAC = "deviceMac";
public static final String CLOUD_PROPERTY_MODEL = "deviceName"; // use name cause modell returns different values
public static final String CLOUD_PROPERTY_NAME = "deviceName";
public static final String CLOUD_PROPERTY_REGION = "deviceRegion";
public static final String CLOUD_PROPERTY_SERVER_URL = "appServerUrl";
public static final String CLOUD_PROPERTY_TYPE = "deviceType";
/*** DEVICE PROPERTY STRINGS (DEVICE) ***/
public static final String DEVICE_PROPERTY_BRIGHTNES = "brightness";
public static final String DEVICE_PROPERTY_COLORTEMP = "color_temp";
public static final String DEVICE_PROPERTY_FW = "fw_ver";
public static final String DEVICE_PROPERTY_HUE = "hue";
public static final String DEVICE_PROPERTY_HW = "hw_ver";
public static final String DEVICE_PROPERTY_ID = "device_id";
public static final String DEVICE_PROPERTY_IP = "ip";
public static final String DEVICE_PROPERTY_MAC = "mac";
public static final String DEVICE_PROPERTY_MODEL = "model";
public static final String DEVICE_PROPERTY_NICKNAME = "nickname";
public static final String DEVICE_PROPERTY_ON = "device_on";
public static final String DEVICE_PROPERTY_ONTIME = "on_time";
public static final String DEVICE_PROPERTY_OVERHEAT = "overheated";
public static final String DEVICE_PROPERTY_REGION = "region";
public static final String DEVICE_PROPERTY_SATURATION = "saturation";
public static final String DEVICE_PROPERTY_SIGNAL = "signal_level";
public static final String DEVICE_PROPERTY_SIGNAL_RSSI = "rssi";
public static final String DEVICE_PROPERTY_TYPE = "type";
public static final String DEVICE_PROPERTY_USAGE_7 = "time_usage_past7";
public static final String DEVICE_PROPERTY_USAGE_30 = "time_usage_past30";
public static final String DEVICE_PROPERTY_USAGE_TODAY = "time_usage_today";
public static final String DEVICE_REPRASENTATION_PROPERTY = "macAddress";
// lightning effects
public static final String DEVICE_PROPERTY_EFFECT = "lighting_effect";
public static final String PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS = "brightness";
public static final String PROPERTY_LIGHTNING_EFFECT_COLORTEMPRANGE = "color_temp_range";
public static final String PROPERTY_LIGHTNING_EFFECT_CUSTOM = "custom";
public static final String PROPERTY_LIGHTNING_EFFECT_DISPLAYCOLORS = "displayColors";
public static final String PROPERTY_LIGHTNING_EFFECT_ENABLE = "enable";
public static final String PROPERTY_LIGHTNING_EFFECT_ID = "id";
public static final String PROPERTY_LIGHTNING_EFFECT_NAME = "name";
/*** DEVICE SETTINGS ***/
public static final Integer BULB_MIN_COLORTEMP = 2500;
public static final Integer BULB_MAX_COLORTEMP = 6500;
/*** CHANNEL LISTS ***/
// channel group actuator
public static final String CHANNEL_GROUP_ACTUATOR = "actuator";
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMP = "colorTemperature";
public static final String CHANNEL_OUTPUT = "output";
public static final String CHANNEL_SWITCH = "switch";
// channel group device
public static final String CHANNEL_GROUP_DEVICE = "device";
public static final String CHANNEL_ONTIME = "onTime";
public static final String CHANNEL_OVERHEAT = "overheated";
public static final String CHANNEL_WIFI_STRENGTH = "wifiSignal";
// channel group effect
public static final String CHANNEL_GROUP_EFFECTS = "effect";
public static final String CHANNEL_FX_BRIGHTNESS = "brightness";
public static final String CHANNEL_FX_COLORS = "displayColors";
public static final String CHANNEL_FX_CUSTOM = "custom";
public static final String CHANNEL_FX_ENABLE = "enable";
public static final String CHANNEL_FX_NAME = "name";
/*** LIST OF PROPERTY NAMES ***/
public static final String PROPERTY_FAMILY = "deviceFamily";
public static final String PROPERTY_LOCATION = "location";
public static final String PROPERTY_WIFI_LEVEL = "signal-strength";
}

View File

@ -0,0 +1,301 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.tapocontrol.internal.TapoDiscoveryService;
import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
/**
* The {@link TapoBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels with a bridge.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
private final TapoErrorHandler bridgeError = new TapoErrorHandler();
private final TapoBridgeConfiguration config;
private final HttpClient httpClient;
private @Nullable ScheduledFuture<?> startupJob;
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable ScheduledFuture<?> discoveryJob;
private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
private TapoCredentials credentials;
private String uid;
public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
Thing thing = getThing();
this.cloudConnector = new TapoCloudConnector(this, httpClient);
this.config = new TapoBridgeConfiguration(thing);
this.credentials = new TapoCredentials();
this.uid = thing.getUID().toString();
this.httpClient = httpClient;
}
/***********************************
*
* BRIDGE INITIALIZATION
*
************************************/
@Override
/**
* INIT BRIDGE
* set credentials and login cloud
*/
public void initialize() {
this.config.loadSettings();
this.credentials = new TapoCredentials(config.username, config.password);
activateBridge();
}
/**
* ACTIVATE BRIDGE
*/
private void activateBridge() {
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
updateStatus(ThingStatus.UNKNOWN);
// background initialization (delay it a little bit):
this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
}
@Override
public void dispose() {
stopScheduler(this.startupJob);
stopScheduler(this.pollingJob);
stopScheduler(this.discoveryJob);
super.dispose();
}
/**
* ACTIVATE DISCOVERY SERVICE
*/
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(TapoDiscoveryService.class);
}
/**
* Set DiscoveryService
*
* @param discoveryService
*/
public void setDiscoveryService(TapoDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
/***********************************
*
* SCHEDULER
*
************************************/
/**
* delayed OneTime StartupJob
*/
private void delayedStartUp() {
loginCloud();
startCloudScheduler();
startDiscoveryScheduler();
}
/**
* Start CloudLogin Scheduler
*/
protected void startCloudScheduler() {
Integer pollingInterval = config.cloudReconnectIntervalM;
if (pollingInterval > 0) {
logger.trace("{} starting bridge cloud sheduler", this.uid);
this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
TimeUnit.MINUTES);
} else {
stopScheduler(this.pollingJob);
}
}
/**
* Start DeviceDiscovery Scheduler
*/
protected void startDiscoveryScheduler() {
Integer pollingInterval = config.discoveryIntervalM;
if (config.cloudDiscoveryEnabled && pollingInterval > 0) {
logger.trace("{} starting bridge discovery sheduler", this.uid);
this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval,
TimeUnit.MINUTES);
} else {
stopScheduler(this.discoveryJob);
}
}
/**
* Stop scheduler
*
* @param scheduler ScheduledFeature<?> which schould be stopped
*/
protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
if (scheduler != null) {
scheduler.cancel(true);
scheduler = null;
}
}
/***********************************
*
* ERROR HANDLER
*
************************************/
/**
* return device Error
*
* @return
*/
public TapoErrorHandler getError() {
return this.bridgeError;
}
/**
* set device error
*
* @param tapoError TapoErrorHandler-Object
*/
public void setError(TapoErrorHandler tapoError) {
this.bridgeError.set(tapoError);
}
/***********************************
*
* BRIDGE COMMUNICATIONS
*
************************************/
/**
* Login to Cloud
*
* @return
*/
public boolean loginCloud() {
bridgeError.reset(); // reset ErrorHandler
if (!config.username.isBlank() && !config.password.isBlank()) {
logger.debug("{} login with user {}", this.uid, config.username);
if (cloudConnector.login(config.username, config.password)) {
updateStatus(ThingStatus.ONLINE);
return true;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set");
}
return false;
}
/***********************************
*
* DEVICE DISCOVERY
*
************************************/
/**
* START DEVICE DISCOVERY
*/
public void discoverDevices() {
this.discoveryService.startScan();
}
/**
* GET DEVICELIST CONNECTED TO BRIDGE
*
* @return devicelist
*/
public JsonArray getDeviceList() {
JsonArray deviceList = new JsonArray();
if (config.cloudDiscoveryEnabled) {
logger.trace("{} discover devicelist from cloud", this.uid);
deviceList = getDeviceListCloud();
}
return deviceList;
}
/**
* GET DEVICELIST FROM CLOUD
* returns all devices stored in cloud
*
* @return deviceList from cloud
*/
private JsonArray getDeviceListCloud() {
logger.trace("{} getDeviceList from cloud", this.uid);
bridgeError.reset(); // reset ErrorHandler
JsonArray deviceList = new JsonArray();
if (loginCloud()) {
deviceList = this.cloudConnector.getDeviceList();
}
return deviceList;
}
/***********************************
*
* BRIDGE GETTERS
*
************************************/
public TapoCredentials getCredentials() {
return this.credentials;
}
public HttpClient getHttpClient() {
return this.httpClient;
}
public ThingUID getUID() {
return getThing().getUID();
}
public TapoBridgeConfiguration getBridgeConfig() {
return this.config;
}
}

View File

@ -0,0 +1,479 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceConfiguration;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class as base for TAPO-Device device implementations.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public abstract class TapoDevice extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(TapoDevice.class);
protected final TapoErrorHandler deviceError = new TapoErrorHandler();
protected final String uid;
protected TapoDeviceConfiguration config;
protected TapoDeviceInfo deviceInfo;
protected @Nullable ScheduledFuture<?> startupJob;
protected @Nullable ScheduledFuture<?> pollingJob;
protected @NonNullByDefault({}) TapoDeviceConnector connector;
protected @NonNullByDefault({}) TapoBridgeHandler bridge;
/**
* Constructor
*
* @param thing Thing object representing device
*/
protected TapoDevice(Thing thing) {
super(thing);
this.config = new TapoDeviceConfiguration(thing);
this.deviceInfo = new TapoDeviceInfo();
this.uid = getThing().getUID().getAsString();
}
/***********************************
*
* INIT AND SETTINGS
*
************************************/
/**
* INITIALIZE DEVICE
*/
@Override
public void initialize() {
try {
this.config.loadSettings();
Bridge bridgeThing = getBridge();
if (bridgeThing != null) {
BridgeHandler bridgeHandler = bridgeThing.getHandler();
if (bridgeHandler != null) {
this.bridge = (TapoBridgeHandler) bridgeHandler;
this.connector = new TapoDeviceConnector(this, bridge);
}
}
} catch (Exception e) {
logger.debug("({}) configuration error : {}", uid, e.getMessage());
}
TapoErrorHandler configError = checkSettings();
if (!configError.hasError()) {
activateDevice();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError.getMessage());
}
}
/**
* DISPOSE
*/
@Override
public void dispose() {
try {
stopScheduler(this.startupJob);
stopScheduler(this.pollingJob);
connector.logout();
} catch (Exception e) {
// handle exception
}
super.dispose();
}
/**
* ACTIVATE DEVICE
*/
private void activateDevice() {
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
updateStatus(ThingStatus.UNKNOWN);
// background initialization (delay it a little bit):
this.startupJob = scheduler.schedule(this::delayedStartUp, 2000, TimeUnit.MILLISECONDS);
startScheduler();
}
/**
* CHECK SETTINGS
*
* @return TapoErrorHandler with configuration-errors
*/
protected TapoErrorHandler checkSettings() {
TapoErrorHandler configErr = new TapoErrorHandler();
/* check bridge */
if (bridge == null || !(bridge instanceof TapoBridgeHandler)) {
configErr.raiseError(ERR_NO_BRIDGE);
return configErr;
}
/* check ip-address */
if (!config.ipAddress.matches(IPV4_REGEX)) {
configErr.raiseError(ERR_CONF_IP);
return configErr;
}
/* check credentials */
if (!bridge.getCredentials().areSet()) {
configErr.raiseError(ERR_CONF_CREDENTIALS);
return configErr;
}
return configErr;
}
/**
* Checks if the response object contains errors and if so throws an {@link IOException} when an error code was set.
*
* @throws IOException if an error code was set in the response object
*/
protected void checkErrors() throws IOException {
final Integer errorCode = deviceError.getCode();
if (errorCode != 0) {
throw new IOException("Error (" + errorCode + "): " + deviceError.getMessage());
}
}
/***********************************
*
* SCHEDULER
*
************************************/
/**
* delayed OneTime StartupJob
*/
private void delayedStartUp() {
connect();
}
/**
* Start scheduler
*/
protected void startScheduler() {
Integer pollingInterval = this.config.pollingInterval;
if (pollingInterval > 0) {
if (pollingInterval < POLLING_MIN_INTERVAL_S) {
pollingInterval = POLLING_MIN_INTERVAL_S;
}
logger.trace("({}) starScheduler: create job with interval : {}", uid, pollingInterval);
this.pollingJob = scheduler.scheduleWithFixedDelay(this::schedulerAction, pollingInterval, pollingInterval,
TimeUnit.SECONDS);
} else {
stopScheduler(this.pollingJob);
}
}
/**
* Stop scheduler
*
* @param scheduler ScheduledFeature<?> which schould be stopped
*/
protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
if (scheduler != null) {
scheduler.cancel(true);
scheduler = null;
}
}
/**
* Scheduler Action
*/
protected void schedulerAction() {
logger.trace("({}) schedulerAction", uid);
queryDeviceInfo();
}
/***********************************
*
* ERROR HANDLER
*
************************************/
/**
* return device Error
*
* @return
*/
public TapoErrorHandler getError() {
return this.deviceError;
}
/**
* set device error
*
* @param tapoError TapoErrorHandler-Object
*/
public void setError(TapoErrorHandler tapoError) {
this.deviceError.set(tapoError);
handleConnectionState();
}
/***********************************
*
* THING
*
************************************/
/***
* Check if ThingType is model
*
* @param model
* @return
*/
protected Boolean isThingModel(String model) {
try {
ThingTypeUID foundType = new ThingTypeUID(BINDING_ID, model);
ThingTypeUID expectedType = getThing().getThingTypeUID();
return expectedType.equals(foundType);
} catch (Exception e) {
logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
return false;
}
}
/**
* CHECK IF RECEIVED DATA ARE FROM THE EXPECTED DEVICE
* Compare MAC-Adress
*
* @param deviceInfo
* @return true if is the expected device
*/
protected Boolean isExpectedThing(TapoDeviceInfo deviceInfo) {
try {
String expectedThingUID = getThing().getProperties().get(DEVICE_REPRASENTATION_PROPERTY);
String foundThingUID = deviceInfo.getRepresentationProperty();
String foundModel = deviceInfo.getModel();
if (expectedThingUID == null || expectedThingUID.isBlank()) {
return isThingModel(foundModel);
}
/* sometimes received mac was with and sometimes without "-" from device */
expectedThingUID = unformatMac(expectedThingUID);
foundThingUID = unformatMac(foundThingUID);
return expectedThingUID.equals(foundThingUID);
} catch (Exception e) {
logger.warn("({}) verify thing model throws : {}", uid, e.getMessage());
return false;
}
}
/**
* Return ThingUID
*/
public ThingUID getThingUID() {
return getThing().getUID();
}
/***********************************
*
* DEVICE PROPERTIES
*
************************************/
/**
* query device Properties
*/
public void queryDeviceInfo() {
queryDeviceInfo(false);
}
/**
* query device Properties
*
* @param ignoreGap ignore gap to last query. query anyway (force)
*/
public void queryDeviceInfo(boolean ignoreGap) {
deviceError.reset();
if (connector.loggedIn()) {
connector.queryInfo(ignoreGap);
} else {
logger.debug("({}) tried to query DeviceInfo but not loggedIn", uid);
connect();
}
}
/**
* SET DEVICE INFOs to device
*
* @param deviceInfo
*/
public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
this.deviceInfo = deviceInfo;
if (isExpectedThing(deviceInfo)) {
devicePropertiesChanged(deviceInfo);
handleConnectionState();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"found type:'" + deviceInfo.getModel() + "' with mac:'" + deviceInfo.getRepresentationProperty()
+ "'. Check IP-Address");
}
}
/**
* Handle full responsebody received from connector
*
* @param responseBody
*/
public void responsePasstrough(String responseBody) {
}
/**
* UPDATE PROPERTIES
*
* If only one property must be changed, there is also a convenient method
* updateProperty(String name, String value).
*
* @param TapoDeviceInfo
*/
protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
/* device properties */
Map<String, String> properties = editProperties();
properties.put(Thing.PROPERTY_MAC_ADDRESS, deviceInfo.getMAC());
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceInfo.getFirmwareVersion());
properties.put(Thing.PROPERTY_HARDWARE_VERSION, deviceInfo.getHardwareVersion());
properties.put(Thing.PROPERTY_MODEL_ID, deviceInfo.getModel());
properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceInfo.getSerial());
updateProperties(properties);
}
/**
* update channel state
*
* @param channelID
* @param value
*/
public void publishState(String channelID, State value) {
updateState(channelID, value);
}
/***********************************
*
* CONNECTION
*
************************************/
/**
* Connect (login) to device
*
*/
public Boolean connect() {
deviceError.reset();
Boolean loginSuccess = false;
try {
loginSuccess = connector.login();
if (loginSuccess) {
connector.queryInfo();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
}
} catch (Exception e) {
updateStatus(ThingStatus.UNKNOWN);
}
return loginSuccess;
}
/**
* disconnect device
*/
public void disconnect() {
connector.logout();
}
/**
* handle device state by connector error
*/
public void handleConnectionState() {
ThingStatus deviceState = getThing().getStatus();
Integer errorCode = deviceError.getCode();
if (errorCode == 0) {
if (deviceState != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
} else if (LIST_REAUTH_ERRORS.contains(errorCode)) {
connect();
} else if (LIST_COMMUNICATION_ERRORS.contains(errorCode)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
disconnect();
} else if (LIST_CONFIGURATION_ERRORS.contains(errorCode)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
} else {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
}
}
/**
* Return IP-Address of device
*/
public String getIpAddress() {
return this.config.ipAddress;
}
/***********************************
*
* CHANNELS
*
************************************/
/**
* Get ChannelID including group
*
* @param group String channel-group
* @param channel String channel-name
* @return String channelID
*/
protected String getChannelID(String group, String channel) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
return group + "#" + channel;
}
return channel;
}
/**
* Get Channel from ChannelID
*
* @param channelID String channelID
* @return String channel-name
*/
protected String getChannelFromID(ChannelUID channelID) {
String channel = channelID.getIdWithoutGroup();
channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
channel = channel.replace(CHANNEL_GROUP_EFFECTS + "#", "");
return channel;
}
}

View File

@ -0,0 +1,230 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.openhab.binding.tapocontrol.internal.structures.TapoLightEffect;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
/**
* TAPO Smart-Plug-Device.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoLightStrip extends TapoDevice {
private final Logger logger = LoggerFactory.getLogger(TapoLightStrip.class);
/**
* Constructor
*
* @param thing Thing object representing device
*/
public TapoLightStrip(Thing thing) {
super(thing);
}
/**
* handle command sent to device
*
* @param channelUID channelUID command is sent to
* @param command command to be sent
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Boolean refreshInfo = false;
String channel = channelUID.getIdWithoutGroup();
String group = channelUID.getGroupId();
if (command instanceof RefreshType) {
refreshInfo = true;
} else if (group == CHANNEL_GROUP_EFFECTS) {
setLightEffect(channel, command);
refreshInfo = true;
} else {
switch (channel) {
case CHANNEL_OUTPUT:
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, command == OnOffType.ON);
refreshInfo = true;
break;
case CHANNEL_BRIGHTNESS:
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
setBrightness(percent.intValue()); // 0..100% = 0..100
refreshInfo = true;
} else if (command instanceof DecimalType) {
setBrightness(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR_TEMP:
if (command instanceof DecimalType) {
setColorTemp(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR:
if (command instanceof HSBType) {
setColor((HSBType) command);
refreshInfo = true;
}
break;
default:
logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
channelUID.getId());
}
}
/* refreshInfo */
if (refreshInfo) {
queryDeviceInfo(true);
}
}
/**
* SET BRIGHTNESS
*
* @param newBrightness percentage 0-100 of new brightness
*/
protected void setBrightness(Integer newBrightness) {
/* switch off if 0 */
if (newBrightness == 0) {
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, false);
} else {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
connector.sendDeviceCommands(newState);
}
}
/**
* SET COLOR
*
* @param command
*/
protected void setColor(HSBType command) {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_HUE, command.getHue());
newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
connector.sendDeviceCommands(newState);
}
/**
* SET COLORTEMP
*
* @param colorTemp (Integer) in Kelvin
*/
protected void setColorTemp(Integer colorTemp) {
HashMap<String, Object> newState = new HashMap<>();
colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_COLORTEMP, colorTemp);
connector.sendDeviceCommands(newState);
}
/**
* set Light Effect from channel/command
*
* @param channel channel (effect) to set
* @param command command (value) to set
*/
protected void setLightEffect(String channel, Command command) {
TapoLightEffect lightEffect = deviceInfo.getLightEffect();
switch (channel) {
case CHANNEL_FX_BRIGHTNESS:
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
lightEffect.setBrightness(percent.intValue()); // 0..100% = 0..100
} else if (command instanceof DecimalType) {
lightEffect.setBrightness(((DecimalType) command).intValue());
}
break;
case CHANNEL_FX_COLORS:
// comming soon
break;
case CHANNEL_FX_NAME:
lightEffect.setName(command.toString());
break;
case CHANNEL_FX_ENABLE:
lightEffect.setEnable(command == OnOffType.ON);
break;
}
setLightEffects(lightEffect);
}
/**
* SET LIGHTNING EFFECTS
*
* @param lightEffect new lightEffect
*/
protected void setLightEffects(TapoLightEffect lightEffect) {
JsonObject newEffect = new JsonObject();
newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_ENABLE, lightEffect.getEnable());
newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_NAME, lightEffect.getName());
newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS, lightEffect.getBrightness());
newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_COLORTEMPRANGE, lightEffect.getColorTempRange().toString());
newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_DISPLAYCOLORS, lightEffect.getDisplayColors().toString());
newEffect.addProperty(PROPERTY_LIGHTNING_EFFECT_CUSTOM, lightEffect.getCustom());
connector.sendDeviceCommand(DEVICE_PROPERTY_EFFECT, newEffect.toString());
}
/**
* UPDATE PROPERTIES
*
* @param TapoDeviceInfo
*/
@Override
protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
TapoLightEffect lightEffect = deviceInfo.getLightEffect();
super.devicePropertiesChanged(deviceInfo);
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
getPercentType(deviceInfo.getBrightness()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
getDecimalType(deviceInfo.getColorTemp()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
getDecimalType(deviceInfo.getSignalLevel()));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
getQuantityType(deviceInfo.getOnTime(), Units.SECOND));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
// light effect
publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_BRIGHTNESS),
getPercentType(lightEffect.getBrightness()));
publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(lightEffect.getName()));
publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_ENABLE), getOnOffType(lightEffect.getEnable()));
publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_CUSTOM), getOnOffType(lightEffect.getCustom()));
}
}

View File

@ -0,0 +1,169 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TAPO Smart-Plug-Device.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoSmartBulb extends TapoDevice {
private final Logger logger = LoggerFactory.getLogger(TapoSmartBulb.class);
/**
* Constructor
*
* @param thing Thing object representing device
*/
public TapoSmartBulb(Thing thing) {
super(thing);
}
/**
* handle command sent to device
*
* @param channelUID channelUID command is sent to
* @param command command to be sent
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Boolean refreshInfo = false;
String channel = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
refreshInfo = true;
} else {
switch (channel) {
case CHANNEL_OUTPUT:
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, command == OnOffType.ON);
refreshInfo = true;
break;
case CHANNEL_BRIGHTNESS:
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
setBrightness(percent.intValue()); // 0..100% = 0..100
refreshInfo = true;
} else if (command instanceof DecimalType) {
setBrightness(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR_TEMP:
if (command instanceof DecimalType) {
setColorTemp(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR:
if (command instanceof HSBType) {
setColor((HSBType) command);
refreshInfo = true;
}
break;
default:
logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
channelUID.getId());
}
}
/* refreshInfo */
if (refreshInfo) {
queryDeviceInfo(true);
}
}
/**
* SET BRIGHTNESS
*
* @param newBrightness percentage 0-100 of new brightness
*/
protected void setBrightness(Integer newBrightness) {
/* switch off if 0 */
if (newBrightness == 0) {
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, false);
} else {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
connector.sendDeviceCommands(newState);
}
}
/**
* SET COLOR
*
* @param command
*/
protected void setColor(HSBType command) {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_HUE, command.getHue());
newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
connector.sendDeviceCommands(newState);
}
/**
* SET COLORTEMP
*
* @param colorTemp (Integer) in Kelvin
*/
protected void setColorTemp(Integer colorTemp) {
HashMap<String, Object> newState = new HashMap<>();
colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_COLORTEMP, colorTemp);
connector.sendDeviceCommands(newState);
}
/**
* UPDATE PROPERTIES
*
* @param TapoDeviceInfo
*/
@Override
protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
super.devicePropertiesChanged(deviceInfo);
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
getPercentType(deviceInfo.getBrightness()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
getDecimalType(deviceInfo.getColorTemp()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
getDecimalType(deviceInfo.getSignalLevel()));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
getQuantityType(deviceInfo.getOnTime(), Units.SECOND));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TAPO Smart-Plug-Device.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoSmartPlug extends TapoDevice {
private final Logger logger = LoggerFactory.getLogger(TapoSmartPlug.class);
/**
* Constructor
*
* @param thing Thing object representing device
*/
public TapoSmartPlug(Thing thing) {
super(thing);
}
/**
* handle command sent to device
*
* @param channelUID channelUID command is sent to
* @param command command to be sent
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Boolean refreshInfo = false;
/* perform actions */
if (command instanceof RefreshType) {
refreshInfo = true;
} else if (command == OnOffType.ON) {
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, true);
refreshInfo = true;
} else if (command == OnOffType.OFF) {
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, false);
refreshInfo = true;
} else {
logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
channelUID.getId());
}
/* refreshInfo */
if (refreshInfo) {
queryDeviceInfo(true);
}
}
/**
* UPDATE PROPERTIES
*
* @param TapoDeviceInfo
*/
@Override
protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
super.devicePropertiesChanged(deviceInfo);
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
getDecimalType(deviceInfo.getSignalLevel()));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
getQuantityType(deviceInfo.getOnTime(), Units.SECOND));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
}
}

View File

@ -0,0 +1,234 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TAPO Universal-Device
* universal device for testing pruposes
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoUniversalDevice extends TapoDevice {
private final Logger logger = LoggerFactory.getLogger(TapoUniversalDevice.class);
// CHANNEL LIST
public static final String CHANNEL_GROUP_DEBUG = "debug";
public static final String CHANNEL_RESPONSE = "deviceResponse";
public static final String CHANNEL_COMMAND = "deviceCommand";
/**
* Constructor
*
* @param thing Thing object representing device
*/
public TapoUniversalDevice(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command.toString(), channelUID.getId());
Boolean refreshInfo = false;
String channel = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
refreshInfo = true;
} else {
switch (channel) {
case CHANNEL_OUTPUT:
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, command == OnOffType.ON);
refreshInfo = true;
break;
case CHANNEL_BRIGHTNESS:
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
setBrightness(percent.intValue()); // 0..100% = 0..100
refreshInfo = true;
} else if (command instanceof DecimalType) {
setBrightness(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR_TEMP:
if (command instanceof DecimalType) {
setColorTemp(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR:
if (command instanceof HSBType) {
setColor((HSBType) command);
refreshInfo = true;
}
break;
case CHANNEL_COMMAND:
String[] cmd = command.toString().split(":");
if (cmd.length == 1) {
connector.sendCustomQuery(cmd[0]);
} else if (cmd.length == 2) {
connector.sendDeviceCommand(cmd[0], cmd[1]);
} else {
logger.warn("({}) wrong command format '{}'", uid, command.toString());
}
break;
default:
logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
channelUID.getId());
}
}
/* refreshInfo */
if (refreshInfo) {
queryDeviceInfo();
}
}
/**
* SET BRIGHTNESS
*
* @param newBrightness percentage 0-100 of new brightness
*/
protected void setBrightness(Integer newBrightness) {
/* switch off if 0 */
if (newBrightness == 0) {
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, false);
} else {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
connector.sendDeviceCommands(newState);
}
}
/**
* SET COLOR
*
* @param command
*/
protected void setColor(HSBType command) {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_HUE, command.getHue());
newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
connector.sendDeviceCommands(newState);
}
/**
* SET COLORTEMP
*
* @param colorTemp (Integer) in Kelvin
*/
protected void setColorTemp(Integer colorTemp) {
HashMap<String, Object> newState = new HashMap<>();
colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_COLORTEMP, colorTemp);
connector.sendDeviceCommands(newState);
}
/**
* SET DEVICE INFOs to device
*
* @param deviceInfo
*/
@Override
public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
devicePropertiesChanged(deviceInfo);
handleConnectionState();
}
/**
* Handle full responsebody received from connector
*
* @param responseBody
*/
public void responsePasstrough(String responseBody) {
logger.debug("({}) received response {}", uid, responseBody);
publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
}
/**
* UPDATE PROPERTIES
*
* @param TapoDeviceInfo
*/
@Override
protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
super.devicePropertiesChanged(deviceInfo);
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
getPercentType(deviceInfo.getBrightness()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
getDecimalType(deviceInfo.getColorTemp()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
getDecimalType(deviceInfo.getSignalLevel()));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
getQuantityType(deviceInfo.getOnTime(), Units.SECOND));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
}
/***********************************
*
* CHANNELS
*
************************************/
/**
* Get ChannelID including group
*
* @param group String channel-group
* @param channel String channel-name
* @return String channelID
*/
@Override
protected String getChannelID(String group, String channel) {
return group + "#" + channel;
}
/**
* Get Channel from ChannelID
*
* @param channelID String channelID
* @return String channel-name
*/
protected String getChannelFromID(ChannelUID channelID) {
String channel = channelID.getIdWithoutGroup();
channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
return channel;
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.helpers;
import static java.util.Base64.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* MimeEncoder
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class MimeEncode {
public byte[] encode(byte[] src) {
return getMimeEncoder().encode(src);
}
public String encodeToString(byte[] src) {
return getMimeEncoder().encodeToString(src);
}
public byte[] decode(byte[] src) {
return getMimeDecoder().decode(src);
}
public byte[] decode(String src) {
return getMimeDecoder().decode(src);
}
}

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.helpers;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* PAYLOAD BUILDER
* Generates payload for TapoHttp request
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class PayloadBuilder {
public String method = "";
private JsonObject parameters = new JsonObject();
/**
* Set Command
*
* @param command command (method) to send
*/
public void setCommand(String command) {
this.method = command;
}
/**
* Add Parameter
*
* @param name parameter name
* @param value parameter value (typeOf Bool,Number or String)
*/
public void addParameter(String name, Object value) {
if (value instanceof Boolean) {
this.parameters.addProperty(name, (Boolean) value);
} else if (value instanceof Number) {
this.parameters.addProperty(name, (Number) value);
} else {
this.parameters.addProperty(name, value.toString());
}
}
/**
* Get JSON Payload (STRING)
*
* @return String JSON-Payload
*/
public String getPayload() {
Gson gson = new Gson();
JsonObject payload = getJsonPayload();
return gson.toJson(payload);
}
/**
* Get JSON Payload (JSON-Object)
*
* @return JsonObject JSON-Payload
*/
public JsonObject getJsonPayload() {
JsonObject payload = new JsonObject();
long timeMils = System.currentTimeMillis();// * 1000;
payload.addProperty("method", this.method);
payload.add("params", this.parameters);
payload.addProperty("requestTimeMils", timeMils);
return payload;
}
/**
* Flush Parameters
* remove all parameters
*/
public void flushParameters(String command) {
this.parameters = new JsonObject();
}
}

View File

@ -0,0 +1,144 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.helpers;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TAPO-CIPHER
* Based on K4CZP3R's p100-java-poc
*
* @author Christian Wild - Initial Initial contribution
*/
@NonNullByDefault
public class TapoCipher {
private final Logger logger = LoggerFactory.getLogger(TapoCipher.class);
protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
protected static final String CIPHER_ALGORITHM = "AES";
protected static final String CIPHER_CHARSET = "UTF-8";
protected static final String HANDSHAKE_TRANSFORMATION = "RSA/ECB/PKCS1Padding";
protected static final String HANDSHAKE_ALGORITHM = "RSA";
protected static final String HANDSHAKE_CHARSET = "UTF-8";
@NonNullByDefault({})
private Cipher encodeCipher;
@NonNullByDefault({})
private Cipher decodeCipher;
@NonNullByDefault({})
private MimeEncode mimeEncode;
/**
* CREATE NEW EMPTY CIPHER
*/
public TapoCipher() {
}
/**
* CREATE NEW CIPHER WITH KEY AND CREDENTIALS
*
* @param handshakeKey Key from Handshake-Request
* @param credentials TapoCredentials
* @throws Exception
*/
public TapoCipher(String handshakeKey, TapoCredentials credentials) {
setKey(handshakeKey, credentials);
}
/**
* SET NEW KEY AND CREDENTIALS
*
* @param handshakeKey
* @param credentials
*/
public void setKey(String handshakeKey, TapoCredentials credentials) {
logger.trace("Init TapoCipher with key: {} ", handshakeKey);
MimeEncode mimeEncode = new MimeEncode();
try {
byte[] decode = mimeEncode.decode(handshakeKey.getBytes(HANDSHAKE_CHARSET));
byte[] decode2 = mimeEncode.decode(credentials.getPrivateKeyBytes());
Cipher instance = Cipher.getInstance(HANDSHAKE_TRANSFORMATION);
KeyFactory kf = KeyFactory.getInstance(HANDSHAKE_ALGORITHM);
PrivateKey p = kf.generatePrivate(new PKCS8EncodedKeySpec(decode2));
instance.init(Cipher.DECRYPT_MODE, p);
byte[] doFinal = instance.doFinal(decode);
byte[] bArr = new byte[16];
byte[] bArr2 = new byte[16];
System.arraycopy(doFinal, 0, bArr, 0, 16);
System.arraycopy(doFinal, 16, bArr2, 0, 16);
initCipher(bArr, bArr2);
} catch (Exception ex) {
logger.warn("Something went wrong: {}", ex.getMessage());
}
}
/**
* INIT ENCODE/DECDE-CIPHERS
*
* @param bArr
* @param bArr2
* @throws Exception
*/
protected void initCipher(byte[] bArr, byte[] bArr2) throws Exception {
try {
mimeEncode = new MimeEncode();
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, CIPHER_ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
this.encodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
this.decodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
this.encodeCipher.init(1, secretKeySpec, ivParameterSpec);
this.decodeCipher.init(2, secretKeySpec, ivParameterSpec);
} catch (Exception e) {
logger.warn("initChiper failed: {}", e.getMessage());
this.encodeCipher = null;
this.decodeCipher = null;
}
}
/**
* ENCODE STRING
*
* @param str source string to encode
* @return encoded string
* @throws Exception
*/
public String encode(String str) throws Exception {
byte[] doFinal;
doFinal = this.encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET));
String encrypted = mimeEncode.encodeToString(doFinal);
return encrypted.replace("\r\n", "");
}
/**
* DECODE STRING
*
* @param str source string to decode
* @return decoded string
* @throws Exception
*/
public String decode(String str) throws Exception {
byte[] data = mimeEncode.decode(str.getBytes(CIPHER_CHARSET));
byte[] doFinal;
doFinal = this.decodeCipher.doFinal(data);
return new String(doFinal);
}
}

View File

@ -0,0 +1,220 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.helpers;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler class for TAPO Credentials
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoCredentials {
private final Logger logger = LoggerFactory.getLogger(TapoCredentials.class);
private MimeEncode mimeEncoder;
private String encodedPassword = "";
private String encodedEmail = "";
private String publicKey = "";
private String privateKey = "";
private String username = "";
private String password = "";
/**
* INIT CLASS
*
*/
public TapoCredentials() {
this.mimeEncoder = new MimeEncode();
}
/**
* INIT CLASS
*
* @param email E-Mail-adress of Tapo Cloud
* @param passowrd Password of Tapo Cloud
*/
public TapoCredentials(String eMail, String password) {
this.mimeEncoder = new MimeEncode();
setCredectials(eMail, password);
}
/**
* set credentials.
*
* @param username username (eMail-adress) of Tapo Cloud
* @param passowrd Password of Tapo Cloud
*/
public void setCredectials(String eMail, String password) {
try {
this.username = eMail;
this.password = password;
encryptCredentials(eMail, password);
createKeyPair();
} catch (Exception e) {
logger.warn("error init credential class '{}'", e.toString());
}
}
/**
* encrypt credentials.
*
* @param username username (eMail-adress) of Tapo Cloud
* @param passowrd Password of Tapo Cloud
*/
private void encryptCredentials(String username, String password) throws Exception {
logger.trace("encrypt credentials for '{}'", username);
/* Password Encoding */
byte[] byteWord = password.getBytes();
this.encodedPassword = mimeEncoder.encodeToString(byteWord);
/* User Encoding */
String encodedUser = this.shaDigestUsername(username);
byteWord = encodedUser.getBytes("UTF-8");
this.encodedEmail = mimeEncoder.encodeToString(byteWord);
}
/**
* Create Key-Pairs
*
*/
public void createKeyPair() throws NoSuchAlgorithmException {
logger.trace("generating new keypair");
KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
instance.initialize(1024, new SecureRandom());
KeyPair generateKeyPair = instance.generateKeyPair();
this.publicKey = new String(mimeEncoder.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
this.privateKey = new String(mimeEncoder.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
logger.trace("new privateKey: '{}'", this.privateKey);
logger.trace("new ublicKey: '{}'", this.publicKey);
}
/**
* shaDigest USERNAME
*
*/
private String shaDigestUsername(String str) throws NoSuchAlgorithmException {
byte[] bArr = str.getBytes();
byte[] digest = MessageDigest.getInstance("SHA1").digest(bArr);
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
String hexString = Integer.toHexString(b & 255);
if (hexString.length() == 1) {
sb.append("0");
sb.append(hexString);
} else {
sb.append(hexString);
}
}
return sb.toString();
}
/**
* RETURN ENCODED PASSWORD
*
*/
public String getEncodedPassword() {
return encodedPassword;
}
/**
* RETURN ENCODED E-MAIL
*
*/
public String getEncodedEmail() {
return encodedEmail;
}
/**
* RETURN PASSWORD
*
*/
public String getPassword() {
return password;
}
/**
* RETURN Username (E-MAIL)
*
*/
public String getUsername() {
return username;
}
/**
* RETURN PRIVATE-KEY
*
* @return String -----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----
*/
public String getPrivateKey() {
return String.format("-----BEGIN PRIVATE KEY-----%n%s%n-----END PRIVATE KEY-----%n", privateKey);
}
/**
* RETURN PUBLIC KEY
*
* @return String -----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----
*/
public String getPublicKey() {
return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
}
/**
* RETURN PRIVATE-KEY (BYTES)
*
* @return UTF-8 coded byte[] with private key
*/
public byte[] getPrivateKeyBytes() {
try {
return privateKey.getBytes("UTF-8");
} catch (Exception e) {
return new byte[0];
}
}
/**
* RETURN PUBLIC-KEY (BYTES)
*
* @return UTF-8 coded byte[] with private key
*/
public byte[] getPublicKeyBytes() {
try {
return publicKey.getBytes("UTF-8");
} catch (Exception e) {
return new byte[0];
}
}
/**
* CHECK IF CREDENTIALS ARE SET
*
* @return
*/
public Boolean areSet() {
return !(this.username.isEmpty() || this.password.isEmpty());
}
}

View File

@ -0,0 +1,264 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.helpers;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.lang.reflect.Field;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* Class Handling TapoErrors
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoErrorHandler extends Exception {
private static final long serialVersionUID = 0L;
private Integer errorCode = 0;
private String errorMessage = "";
private String infoMessage = "";
private Gson gson = new Gson();
/**
* Constructor
*
*/
public TapoErrorHandler() {
}
/**
* Constructor
*
* @param errorCode error code (number)
*/
public TapoErrorHandler(Integer errorCode) {
raiseError(errorCode);
}
/**
* Constructor
*
* @param errorCode error code (number)
* @param infoMessage optional info-message
*/
public TapoErrorHandler(Integer errorCode, String infoMessage) {
raiseError(errorCode, infoMessage);
}
/**
* Constructor
*
* @param exception Exception
*/
public TapoErrorHandler(Exception ex) {
raiseError(ex);
}
/**
* Constructor
*
* @param exception Exception
* @param infoMessage optional info-message
*/
public TapoErrorHandler(Exception ex, String infoMessage) {
raiseError(ex, infoMessage);
}
/***********************************
*
* Private Functions
*
************************************/
/**
* GET ERROR-MESSAGE
*
* @param errCode error Number (or constant ERR_CODE )
* @return error-message if set constant ERR_CODE_MSG. if not name of ERR_CODE is returned
*/
private String getErrorMessage(Integer errCode) {
Field[] fields = TapoErrorConstants.class.getDeclaredFields();
/* loop ErrorConstants and search for code in value */
for (Field f : fields) {
String constName = f.getName();
try {
Integer val = (Integer) f.get(this);
if (val != null && val.equals(errCode)) {
Field constantName = TapoErrorConstants.class.getDeclaredField(constName + "_MSG");
String msg = getValueOrDefault(constantName.get(null), "").toString();
if (msg.length() > 2) {
return msg;
} else {
return infoMessage + " (" + constName + ")";
}
}
} catch (Exception e) {
// next loop
}
}
return infoMessage + " (" + errCode.toString() + ")";
}
/***********************************
*
* Public Functions
*
************************************/
/**
* Raises new error
*
* @param errorCode error code (number)
*/
public void raiseError(Integer errorCode) {
raiseError(errorCode, "");
}
/**
* Raises new error
*
* @param errorCode error code (number)
* @param infoMessage optional info-message
*/
public void raiseError(Integer errorCode, String infoMessage) {
this.errorCode = errorCode;
this.infoMessage = infoMessage;
this.errorMessage = getErrorMessage(errorCode);
}
/**
* Raises new error
*
* @param exception Exception
*/
public void raiseError(Exception ex) {
raiseError(ex, "");
}
/**
* Raises new error
*
* @param exception Exception
* @param infoMessage optional info-message
*/
public void raiseError(Exception ex, String infoMessage) {
this.errorCode = ex.hashCode();
this.infoMessage = infoMessage;
this.errorMessage = getValueOrDefault(ex.getMessage(), ex.toString());
}
/**
* Take over tapoError
*
* @param tapoError
*/
public void set(TapoErrorHandler tapoError) {
this.errorCode = tapoError.getNumber();
this.infoMessage = tapoError.getExtendedInfo();
this.errorMessage = getErrorMessage(this.errorCode);
}
/**
* Reset Error
*/
public void reset() {
this.errorCode = 0;
this.errorMessage = "";
this.infoMessage = "";
}
/***********************************
*
* GET RESULTS
*
************************************/
/**
* Get Error Message
*
* @return error text
*/
@Override
@Nullable
public String getMessage() {
return this.errorMessage;
}
/**
* Get Error Message directly by error-number
*
* @param errorCode
* @return error message
*/
public String getMessage(Integer errorCode) {
return getErrorMessage(errorCode);
}
/**
* Get Error Code
*
* @return error code (integer)
*/
public Integer getCode() {
return this.errorCode;
}
/**
* Get Info Message
*
* @return error extended info
*/
public String getExtendedInfo() {
return this.infoMessage;
}
/**
* Get Error Number
*
* @return error number
*/
public Integer getNumber() {
return this.errorCode;
}
/**
* Check if has Error
*
* @return true if has error
*/
public Boolean hasError() {
return this.errorCode != 0;
}
/**
* Get JSON-Object with errror
*
* @return JsonObject with error-informations
*/
public JsonObject getJson() {
JsonObject json;
json = gson.fromJson("{'error_code': '" + errorCode + "', 'error_message':'" + errorMessage + "'}",
JsonObject.class);
if (json == null) {
json = new JsonObject();
}
return json;
}
}

View File

@ -0,0 +1,348 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.helpers;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
/**
* {@link TapoUtils} TapoUtils -
* Utility Helper Functions
*
* @author Christian Wild - Initial Initial contribution
*/
@NonNullByDefault
public class TapoUtils {
/************************************
* CALCULATION UTILS
***********************************/
/**
* Limit Value between limits
*
* @param value Integer
* @param lowerLimit
* @param upperLimit
* @return
*/
public static Integer limitVal(@Nullable Integer value, Integer lowerLimit, Integer upperLimit) {
if (value == null || value < lowerLimit) {
return lowerLimit;
} else if (value > upperLimit) {
return upperLimit;
}
return value;
}
/************************************
* FORMAT UTILS
***********************************/
/**
* return value or default val if it's null
*
* @param <T> Type of value
* @param value value
* @param defaultValue defaut value
* @return
*/
public static <T> T getValueOrDefault(@Nullable T value, T defaultValue) {
return value == null ? defaultValue : value;
}
/**
* Format MAC-Address replacing old division chars and add new one
*
* @param mac unformated mac-Address
* @param newDivisionChar new division char (e.g. ":","-" )
* @return new formated mac-Address
*/
public static String formatMac(String mac, char newDivisionChar) {
String unformatedMac = unformatMac(mac);
String formatedMac = unformatedMac.replaceAll("(.{2})", "$1" + newDivisionChar).substring(0, 17);
return formatedMac;
}
/**
* unformat MAC-Address replace all division chars
*
* @param mac
* @return
*/
public static String unformatMac(String mac) {
mac = mac.replace("-", "");
mac = mac.replace(":", "");
mac = mac.replace(".", "");
return mac;
}
/**
* HEX-STRING to byte convertion
*/
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
try {
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
} catch (Exception e) {
}
return data;
}
/**
* Return Boolean from string
*
* @param s - string to be converted
* @param defVal - Default Value
*/
public Boolean stringToBool(@Nullable String s, boolean defVal) {
if (s == null) {
return defVal;
}
try {
return Boolean.parseBoolean(s);
} catch (Exception e) {
return defVal;
}
}
/**
* Return Integer from string
*
* @param s - string to be converted
* @param defVal - Default Value
*/
public Integer stringToInteger(@Nullable String s, Integer defVal) {
if (s == null) {
return defVal;
}
try {
return Integer.valueOf(s);
} catch (Exception e) {
return defVal;
}
}
/***********************************
* JSON-FORMATER
************************************/
public static boolean isValidJson(String json) {
try {
Gson gson = new Gson();
JsonObject jsnObject = gson.fromJson(json, JsonObject.class);
return jsnObject != null;
} catch (Exception e) {
return false;
}
}
/**
*
* @param name parameter name
* @param defVal - default value;
* @return string value
*/
public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name, String defVal) {
if (jsonObject != null && jsonObject.has(name)) {
return jsonObject.get(name).getAsString();
} else {
return defVal;
}
}
/**
*
* @param name parameter name
* @return string value
*/
public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name) {
return jsonObjectToString(jsonObject, name, "");
}
/**
*
* @param name parameter name
* @param defVal - default value;
* @return boolean value
*/
public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name, Boolean defVal) {
if (jsonObject != null && jsonObject.has(name)) {
return jsonObject.get(name).getAsBoolean();
} else {
return false;
}
}
/**
*
* @param name parameter name
* @return boolean value
*/
public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name) {
return jsonObjectToBool(jsonObject, name, false);
}
/**
*
* @param name parameter name
* @param defVal - default value;
* @return integer value
*/
public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name, Integer defVal) {
if (jsonObject != null && jsonObject.has(name)) {
return jsonObject.get(name).getAsInt();
} else {
return defVal;
}
}
/**
*
* @param name parameter name
* @return integer value
*/
public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name) {
return jsonObjectToInt(jsonObject, name, 0);
}
/**
*
* @param name parameter name
* @param defVal - default value;
* @return number value
*/
public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name, Number defVal) {
if (jsonObject != null && jsonObject.has(name)) {
return jsonObject.get(name).getAsNumber();
} else {
return defVal;
}
}
/**
*
* @param name parameter name
* @return number value
*/
public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name) {
return jsonObjectToNumber(jsonObject, name, 0);
}
/************************************
* TYPE UTILS
***********************************/
/**
* Return OnOffType from bool
*
* @param boolVal
*/
public static OnOffType getOnOffType(@Nullable Boolean boolVal) {
return (boolVal != null ? boolVal ? OnOffType.ON : OnOffType.OFF : OnOffType.OFF);
}
/**
* Return OnOffType from bool
*
* @param boolVal
*/
public static OnOffType getOnOffType(Integer intVal) {
return intVal == 0 ? OnOffType.OFF : OnOffType.ON;
}
/**
* Return StringType from String
*
* @param strVal
*/
public static StringType getStringType(@Nullable String strVal) {
return new StringType(strVal != null ? strVal : "");
}
/**
* Return DecimalType from Double
*
* @param numVal
*/
public static DecimalType getDecimalType(@Nullable Double numVal) {
return new DecimalType((numVal != null ? numVal : 0));
}
/**
* Return DecimalType from Integer
*
* @param numVal
*/
public static DecimalType getDecimalType(@Nullable Integer numVal) {
return new DecimalType((numVal != null ? numVal : 0));
}
/**
* Return DecimalType from Long
*
* @param numVal
*/
public static DecimalType getDecimalTypel(@Nullable Long numVal) {
return new DecimalType((numVal != null ? numVal : 0));
}
/**
*
* @param numVal value 0-100
* @return PercentType
*/
public static PercentType getPercentType(@Nullable Integer numVal) {
Integer val = limitVal(numVal, 0, 100);
return new PercentType(val);
}
/**
* Return HSBType from integers
*
* @param hue integer hue-color
* @param saturation integer saturation
* @param brightness integer brightness
* @return HSBType
*/
public static HSBType getHSBType(Integer hue, Integer saturation, Integer brightness) {
DecimalType h = new DecimalType(hue);
PercentType s = new PercentType(saturation);
PercentType b = new PercentType(brightness);
return new HSBType(h, s, b);
}
/**
* Return QuantityType with Time
*
* @param numVal Number with value
* @param unit TimeUnit (Unit<Time>)
* @return QuantityTime<Time>
*/
public static QuantityType<Time> getQuantityType(@Nullable Number numVal, Unit<Time> unit) {
return new QuantityType<>((numVal != null ? numVal : 0), unit);
}
}

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public final class TapoBridgeConfiguration {
private final Logger logger = LoggerFactory.getLogger(TapoBridgeConfiguration.class);
/* THING CONFIGUTATION PROPERTYS */
public static final String CONFIG_EMAIL = "username";
public static final String CONFIG_PASS = "password";
public static final String CONFIG_DEVICE_IP = "ipAddress";
public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
/* DEFAULT & FIXED CONFIGURATIONS */
public static final Integer CONFIG_CLOUD_FIXED_INTERVAL = 1440;
/* thing configuration parameter. */
public String username = "";
public String password = "";
public Boolean cloudDiscoveryEnabled = false;
public Boolean udpDiscoveryEnabled = false;
public Integer cloudReconnectIntervalM = CONFIG_CLOUD_FIXED_INTERVAL;
public Integer discoveryIntervalM = 30;
private Thing bridge;
/**
* Create settings
*
* @param thing BridgeThing
*/
public TapoBridgeConfiguration(Thing thing) {
this.bridge = thing;
loadSettings();
}
/**
* LOAD SETTINGS
*/
public void loadSettings() {
try {
Configuration config = this.bridge.getConfiguration();
username = config.get(CONFIG_EMAIL).toString();
password = config.get(CONFIG_PASS).toString();
cloudDiscoveryEnabled = Boolean.parseBoolean(config.get(CONFIG_DISCOVERY_CLOUD).toString());
discoveryIntervalM = Integer.valueOf(config.get(CONFIG_DISCOVERY_INTERVAL).toString());
} catch (Exception e) {
logger.warn("{} error reading configuration: '{}'", bridge.getUID(), e.getMessage());
}
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public final class TapoDeviceConfiguration {
private final Logger logger = LoggerFactory.getLogger(TapoDeviceConfiguration.class);
/* THING CONFIGUTATION PROPERTYS */
public static final String CONFIG_DEVICE_IP = "ipAddress";
public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
/* thing configuration parameter. */
public String ipAddress = "";
public Integer pollingInterval = 30;
private final Thing device;
/**
* Create settings
*
* @param thing BridgeThing
*/
public TapoDeviceConfiguration(Thing thing) {
this.device = thing;
loadSettings();
}
/**
* LOAD SETTINGS
*/
public void loadSettings() {
try {
Configuration config = this.device.getConfiguration();
this.ipAddress = config.get(CONFIG_DEVICE_IP).toString();
this.pollingInterval = Integer.valueOf(config.get(CONFIG_UPDATE_INTERVAL).toString());
} catch (Exception e) {
logger.warn("{} error reading device-configuration: '{}'", device.getUID().toString(), e.getMessage());
}
}
}

View File

@ -0,0 +1,224 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import com.google.gson.JsonObject;
/**
* Tapo-Device Information class
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoDeviceInfo {
private Boolean deviceOn = false;
private Boolean overheated = false;
private Integer brightness = 0;
private Integer colorTemp = 0;
private Integer hue = 0;
private Integer rssi = 0;
private Integer saturation = 100;
private Integer signalLevel = 0;
private Number onTime = 0;
private Number timeUsagePast7 = 0;
private Number timeUsagePast30 = 0;
private Number timeUsageToday = 0;
private String deviceId = "";
private String fwVer = "";
private String hwVer = "";
private String ip = "";
private String mac = "";
private String model = "";
private String nickname = "";
private String region = "";
private String type = "";
private TapoLightEffect lightEffect = new TapoLightEffect();
private JsonObject jsonObject = new JsonObject();
/**
* INIT
*/
public TapoDeviceInfo() {
setData();
}
/**
* Init DeviceInfo with new Data;
*
* @param jso JsonObject new Data
*/
public TapoDeviceInfo(JsonObject jso) {
jsonObject = jso;
setData();
}
/**
* Set Data (new JsonObject)
*
* @param jso JsonObject new Data
*/
public TapoDeviceInfo setData(JsonObject jso) {
this.jsonObject = jso;
setData();
return this;
}
private void setData() {
this.brightness = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_BRIGHTNES);
this.colorTemp = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_COLORTEMP, BULB_MIN_COLORTEMP);
this.deviceId = jsonObjectToString(jsonObject, DEVICE_PROPERTY_ID);
this.deviceOn = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_ON);
this.fwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_FW);
this.hue = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_HUE);
this.hwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_HW);
this.ip = jsonObjectToString(jsonObject, DEVICE_PROPERTY_IP);
this.lightEffect = lightEffect.setData(jsonObject);
this.mac = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MAC);
this.model = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MODEL);
this.nickname = jsonObjectToString(jsonObject, DEVICE_PROPERTY_NICKNAME);
this.onTime = jsonObjectToNumber(jsonObject, DEVICE_PROPERTY_ONTIME);
this.overheated = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_OVERHEAT);
this.region = jsonObjectToString(jsonObject, DEVICE_PROPERTY_REGION);
this.saturation = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SATURATION);
this.signalLevel = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL);
this.rssi = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL_RSSI);
this.timeUsagePast7 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_7);
this.timeUsagePast30 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_30);
this.timeUsageToday = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_TODAY);
this.type = jsonObjectToString(jsonObject, DEVICE_PROPERTY_TYPE);
}
/***********************************
*
* GET VALUES
*
************************************/
public Integer getBrightness() {
return brightness;
}
public Integer getColorTemp() {
return colorTemp;
}
public String getFirmwareVersion() {
return fwVer;
}
public String getHardwareVersion() {
return hwVer;
}
public HSBType getHSB() {
DecimalType h = new DecimalType(hue);
PercentType s = new PercentType(saturation);
PercentType b = new PercentType(brightness);
return new HSBType(h, s, b);
}
public Integer getHue() {
return hue;
}
public TapoLightEffect getLightEffect() {
return lightEffect;
}
public String getIP() {
return ip;
}
public Boolean isOff() {
return !deviceOn;
}
public Boolean isOn() {
return deviceOn;
}
public Boolean isOverheated() {
return overheated;
}
public String getMAC() {
return formatMac(mac, MAC_DIVISION_CHAR);
}
public String getModel() {
return model.replace(" ", "_");
}
public String getNickname() {
return nickname;
}
public Number getOnTime() {
return onTime;
}
public String getRegion() {
return region;
}
public String getRepresentationProperty() {
return getMAC();
}
public Integer getSaturation() {
return saturation;
}
public String getSerial() {
return deviceId;
}
public Integer getSignalLevel() {
return signalLevel;
}
public Integer getRSSI() {
return rssi;
}
public Number getTimeUsagePast7() {
return timeUsagePast7;
}
public Number getTimeUsagePast30() {
return timeUsagePast30;
}
public Number getTimeUsagePastToday() {
return timeUsageToday;
}
public String getType() {
return type;
}
@Override
public String toString() {
return jsonObject.toString();
}
}

View File

@ -0,0 +1,141 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.awt.Color;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonObject;
/**
* Tapo-LightningEffect Structure Class
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoLightEffect {
private Integer enable = 0;
private String id = "";
private String name = "";
private Integer custom = 0;
private Integer brightness = 0;
private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
private Color displayColors[] = { Color.WHITE };
private JsonObject jsonObject = new JsonObject();
/**
* INIT
*/
public TapoLightEffect() {
setData();
}
/**
* Init DeviceInfo with new Data;
*
* @param jso JsonObject new Data
*/
public TapoLightEffect(JsonObject jso) {
setData(jso);
}
/**
* Set Data (new JsonObject)
*
* @param jso JsonObject new Data
*/
public TapoLightEffect setData(JsonObject jso) {
/* create empty jsonObject to set efault values if has no lighning effect */
if (jsonObject.has(DEVICE_PROPERTY_EFFECT)) {
this.jsonObject = jso;
} else {
jsonObject = new JsonObject();
}
setData();
return this;
}
private void setData() {
this.enable = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_ENABLE);
this.id = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_ID);
this.name = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_NAME);
this.custom = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_CUSTOM); // jsonObjectToBool
this.brightness = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS);
}
/***********************************
*
* SET VALUES
*
************************************/
public void setEnable(Boolean enable) {
this.enable = enable ? 1 : 0;
}
public void setName(String value) {
this.name = value;
}
public void setCustom(Boolean enable) {
this.custom = enable ? 1 : 0;
}
public void setBrightness(Integer value) {
this.brightness = value;
}
/***********************************
*
* GET VALUES
*
************************************/
public Integer getEnable() {
return this.enable;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public Integer getCustom() {
return this.custom;
}
public Integer getBrightness() {
return this.brightness;
}
public Integer[] getColorTempRange() {
return this.colorTempRange;
}
public Color[] getDisplayColors() {
return this.displayColors;
}
@Override
public String toString() {
return jsonObject.toString();
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="tapocontrol" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>TapoControl Binding</name>
<description>Control your TAPO-SmartHome Devices</description>
</binding:binding>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:tapo:device">
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP Address</label>
</parameter>
<parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
<label>Refresh Interval</label>
<description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
<default>30</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="bridge-type:tapo:bridge">
<parameter name="username" type="text" required="true">
<context>email</context>
<label>Username</label>
<description>Tapo-Cloud Login User (e-Mail)</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
<description>Tapo-Cloud Login Password</description>
</parameter>
<parameter name="cloudDiscovery" type="boolean" required="false">
<label>Cloud Discovery</label>
<description>Use Cloud Discovery-Service</description>
<default>false</default>
<advanced>false</advanced>
</parameter>
<parameter name="discoveryInterval" type="integer" min="1" max="10080" required="false">
<label>Background Discovery Interval</label>
<description>Interval background discovery in minutes (default 60)</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- L510E THING-TYPE (WHITE-LIGHT-BULB) -->
<thing-type id="L510_Series">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>L510 Series White-Bulb</label>
<description>Tapo Smart dimmable White-Light-Bulb</description>
<channel-groups>
<channel-group id="actuator" typeId="lightBulb"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- L530 Series THING-TYPE (COLOR-LIGHT-BULB) -->
<thing-type id="L530_Series">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>L530 Series Color-Bulb</label>
<description>Tapo Smart Multicolor Light-Bulb</description>
<channel-groups>
<channel-group id="actuator" typeId="colorBulb"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- L530 Series THING-TYPE (COLOR-LIGHT-BULB) -->
<thing-type id="L900">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>L900 LightStrip</label>
<description>Tapo Smart Multicolor Light-Lightstrip</description>
<channel-groups>
<channel-group id="actuator" typeId="lightStrip"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- P100 THING-TYPE (SOCKET) -->
<thing-type id="P100">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>P100 SmartPlug</label>
<description>Tapo Smart Wifi Plug</description>
<channel-groups>
<channel-group id="actuator" typeId="smartPlug"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- P100 THING-TYPE (SOCKET) -->
<thing-type id="P105">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>P105 SmartPlug</label>
<description>Tapo Mini Smart Wifi Plug</description>
<channel-groups>
<channel-group id="actuator" typeId="smartPlug"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>Cloud-Login</label>
<description>Cloud Connector. Acts as device-bridge</description>
<config-description-ref uri="bridge-type:tapo:bridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- ############################### CHANNEL-GROUPS ############################### -->
<!-- CHANNEL GROUP TYPES -->
<!--Device-Statuss Channel Type -->
<channel-group-type id="deviceState">
<label>Device State</label>
<description>Information about the device</description>
<channels>
<channel id="wifiSignal" typeId="system.signal-strength"/>
<channel id="onTime" typeId="ontime"/>
<channel id="overheated" typeId="overheated"/>
</channels>
</channel-group-type>
<!--Actor Channel Type -->
<channel-group-type id="smartPlug">
<label>SmartPlug</label>
<description>Tapo Smart Plug Power Outlet</description>
<channels>
<channel id="output" typeId="outputChannel"/>
</channels>
</channel-group-type>
<!--Light-Bulb Channel Type -->
<channel-group-type id="lightBulb">
<label>Light Bulb</label>
<description>Tapo Smart Light Bulb</description>
<channels>
<channel id="output" typeId="lightOn"/>
<channel id="brightness" typeId="dimmerChannel"/>
<channel id="colorTemperature" typeId="colorTemperature"/>
</channels>
</channel-group-type>
<!--Color Channel Type -->
<channel-group-type id="colorBulb">
<label>Color Light Bulb</label>
<description>Tapo Multicolor Smart Light Bulb</description>
<channels>
<channel id="output" typeId="lightOn"/>
<channel id="brightness" typeId="dimmerChannel"/>
<channel id="color" typeId="colorChannel"/>
<channel id="colorTemperature" typeId="colorTemperature"/>
</channels>
</channel-group-type>
<!-- LightStrip -->
<channel-group-type id="lightStrip">
<label>Color Light Strip</label>
<description>Tapo Multicolor Smart Light Strip</description>
<channels>
<channel id="output" typeId="lightOn"/>
<channel id="brightness" typeId="dimmerChannel"/>
<channel id="color" typeId="colorChannel"/>
<channel id="colorTemperature" typeId="colorTemperature"/>
</channels>
</channel-group-type>
<!-- Lightning Effect -->
<channel-group-type id="lightEffect">
<label>Lightning Effect</label>
<description>Tapo Lightning Effects</description>
<channels>
<channel id="enable" typeId="effectOn"/>
<channel id="brightness" typeId="dimmerChannel"/>
<channel id="name" typeId="effectName"/>
<channel id="custom" typeId="customEffect"/>
<channel id="displayColor1" typeId="colorChannel"/>
<channel id="displayColor2" typeId="colorChannel"/>
<channel id="displayColor3" typeId="colorChannel"/>
<channel id="displayColor4" typeId="colorChannel"/>
</channels>
</channel-group-type>
<!-- ############################### CHANNELS ############################### -->
<!-- ACTOR CHANNEL TYPES -->
<!-- OuputState Channel Type -->
<channel-type id="outputChannel">
<item-type>Switch</item-type>
<label>Output Switch</label>
<description>Switches the power state on/off</description>
<category>PowerOutlet</category>
<state readOnly="false"/>
</channel-type>
<!-- LightOn/Off Channel Type -->
<channel-type id="lightOn">
<item-type>Switch</item-type>
<label>Light On</label>
<description>Switches the light on/off</description>
<category>LightBulb</category>
<state readOnly="false"/>
</channel-type>
<!-- Dimmer Channel Type -->
<channel-type id="dimmerChannel">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<description>Brightness</description>
<category>LightBulb</category>
<state readOnly="false"/>
</channel-type>
<!-- Color Channel Type -->
<channel-type id="colorChannel">
<item-type>Color</item-type>
<label>Color</label>
<description>Color</description>
<category>ColorLight</category>
<state readOnly="false"/>
</channel-type>
<!-- Color Temperature -->
<channel-type id="colorTemperature">
<item-type>Number</item-type>
<label>Color Temperature</label>
<description>This channel supports adjusting the color temperature from 2700K to 6500K.</description>
<category>LightBulb</category>
<state min="2500" max="6500" pattern="%d K"/>
</channel-type>
<!-- DEVICE-STATE CHANNEL TYPES -->
<!-- uptime -->
<channel-type id="ontime" advanced="true">
<item-type>Number:Time</item-type>
<label>On-Time</label>
<description>Number of seconds since the device was powered on</description>
<category>Time</category>
<state readOnly="true" pattern="%s %unit%"/>
</channel-type>
<!-- overheated -->
<channel-type id="overheated" advanced="true">
<item-type>Switch</item-type>
<label>Device Overheated</label>
<description>ON if device is overheated</description>
<category>Alarm</category>
<state readOnly="true"/>
</channel-type>
<!-- LightningEffect Channel Type -->
<!-- effect on -->
<channel-type id="effectOn">
<item-type>Switch</item-type>
<label>Lightning Effect Enable</label>
<description>Switches the lightning effect on/off</description>
<category>LightBulb</category>
<state readOnly="false"/>
</channel-type>
<!-- effect name -->
<channel-type id="effectName">
<item-type>String</item-type>
<label>Effect Name</label>
<description>Name of LightningEffect</description>
<state readOnly="false"/>
</channel-type>
<!-- custom effect -->
<channel-type id="customEffect">
<item-type>Switch</item-type>
<label>Custom Effect</label>
<description>Use custom lightning effect</description>
<category>LightBulb</category>
<state readOnly="false"/>
</channel-type>
<!-- ADVANCED SETTING CHANNELS -->
<!-- device led -->
<channel-type id="led" advanced="true">
<item-type>Switch</item-type>
<label>Switch Led</label>
<description>Switch the Smart Home device led on or off.</description>
<category>Switch</category>
</channel-type>
<!-- fade light -->
<channel-type id="fade" advanced="true">
<item-type>Switch</item-type>
<label>Fade Light</label>
<description>Make the light darker or lighter slowly</description>
<category>Switch</category>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,138 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.api;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* Handler class for TAPO Smart Home device UDP-connections.
* THIS IS FOR TESTING
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoUDP {
private final Logger logger = LoggerFactory.getLogger(TapoUDP.class);
private static final Integer BROADCAST_TIMEOUT_MS = 5000;
private static final Integer BROADCAST_DISCOVERY_PORT = 20002; // int
private static final String BROADCAST_IP = "255.255.255.255";
private static final String DISCOVERY_MESSAGE_KEY = "rsa_key";
private static final String DISCOVERY_MESSAGE_START_BYTES = "0200000101e5110001cb8c577dd7deb8";
private static final Integer BUFFER_SIZE = 501;
private TapoCredentials credentials;
public TapoUDP(TapoCredentials credentials) {
this.credentials = credentials; // new TapoCredentials();
}
public JsonArray udpScan() {
try {
DatagramSocket udpSocket = new DatagramSocket();
udpSocket.setSoTimeout(BROADCAST_TIMEOUT_MS);
udpSocket.setBroadcast(true);
/* create payload for handshake */
String publicKey = credentials.getPublicKey();
publicKey = generateOwnRSAKey(); // credentials.getPublicKey();
JsonObject parameters = new JsonObject();
JsonObject messageObject = new JsonObject();
parameters.addProperty(DISCOVERY_MESSAGE_KEY, publicKey);
messageObject.add("params", parameters);
String discoveryMessage = messageObject.toString();
byte[] startByte = hexStringToByteArray(DISCOVERY_MESSAGE_START_BYTES);
byte[] message = discoveryMessage.getBytes("UTF-8");
byte[] sendData = new byte[startByte.length + message.length];
System.arraycopy(startByte, 0, sendData, 0, startByte.length);
System.arraycopy(message, 0, sendData, startByte.length, message.length);
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
InetAddress.getByName(BROADCAST_IP), BROADCAST_DISCOVERY_PORT);
udpSocket.send(sendPacket);
while (true) {
// Wait for a response
byte[] recvBuf = new byte[BUFFER_SIZE];
DatagramPacket receivePacket;
try {
receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
udpSocket.receive(receivePacket);
} catch (SocketTimeoutException e) {
udpSocket.close();
return new JsonArray();
} catch (Exception e) {
udpSocket.close();
return new JsonArray();
}
// Check if the message is correct
String responseMessage = new String(receivePacket.getData(), "UTF-8").trim();
if (responseMessage.length() == 0) {
udpSocket.close();
}
String addressBC = receivePacket.getAddress().getHostAddress();
gotDeviceAdress(addressBC);
}
} catch (Exception e) {
// handle exception
}
return new JsonArray();
}
private void gotDeviceAdress(String ipAddress) {
// handle exception
}
private String generateOwnRSAKey() {
try {
logger.trace("generating new keypair");
KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
instance.initialize(1536, new SecureRandom());
KeyPair generateKeyPair = instance.generateKeyPair();
String publicKey = new String(java.util.Base64.getMimeEncoder()
.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
String privateKey = new String(java.util.Base64.getMimeEncoder()
.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
logger.trace("new privateKey: '{}'", privateKey);
logger.trace("new ublicKey: '{}'", publicKey);
return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
} catch (Exception e) {
// couldn't generate own rsa key
return "";
}
}
}

View File

@ -0,0 +1,317 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.tapocontrol.internal.TapoDiscoveryService;
import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
import org.openhab.binding.tapocontrol.internal.api.TapoUDP;
import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
/**
* The {@link TapoBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels with a bridge.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(TapoBridgeHandler.class);
private final TapoErrorHandler bridgeError = new TapoErrorHandler();
private final TapoBridgeConfiguration config;
private final HttpClient httpClient;
private @Nullable ScheduledFuture<?> startupJob;
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable ScheduledFuture<?> discoveryJob;
private @NonNullByDefault({}) TapoCloudConnector cloudConnector;
private @NonNullByDefault({}) TapoDiscoveryService discoveryService;
private TapoCredentials credentials;
private String uid;
public TapoBridgeHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
Thing thing = getThing();
this.cloudConnector = new TapoCloudConnector(this, httpClient);
this.config = new TapoBridgeConfiguration(thing);
this.credentials = new TapoCredentials();
this.uid = thing.getUID().toString();
this.httpClient = httpClient;
}
/***********************************
*
* BRIDGE INITIALIZATION
*
************************************/
@Override
/**
* INIT BRIDGE
* set credentials and login cloud
*/
public void initialize() {
this.config.loadSettings();
this.credentials = new TapoCredentials(config.username, config.password);
activateBridge();
}
/**
* ACTIVATE BRIDGE
*/
private void activateBridge() {
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
updateStatus(ThingStatus.UNKNOWN);
// background initialization (delay it a little bit):
this.startupJob = scheduler.schedule(this::delayedStartUp, 1000, TimeUnit.MILLISECONDS);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("{} Bridge doesn't handle command: {}", this.uid, command);
}
@Override
public void dispose() {
stopScheduler(this.startupJob);
stopScheduler(this.pollingJob);
stopScheduler(this.discoveryJob);
super.dispose();
}
/**
* ACTIVATE DISCOVERY SERVICE
*/
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(TapoDiscoveryService.class);
}
/**
* Set DiscoveryService
*
* @param discoveryService
*/
public void setDiscoveryService(TapoDiscoveryService discoveryService) {
this.discoveryService = discoveryService;
}
/***********************************
*
* SCHEDULER
*
************************************/
/**
* delayed OneTime StartupJob
*/
private void delayedStartUp() {
loginCloud();
startCloudScheduler();
startDiscoveryScheduler();
}
/**
* Start CloudLogin Scheduler
*/
protected void startCloudScheduler() {
Integer pollingInterval = config.cloudReconnectIntervalM;
if (pollingInterval > 0) {
logger.trace("{} starting bridge cloud sheduler", this.uid);
this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
TimeUnit.MINUTES);
} else {
stopScheduler(this.pollingJob);
}
}
/**
* Start DeviceDiscovery Scheduler
*/
protected void startDiscoveryScheduler() {
Integer pollingInterval = config.discoveryIntervalM;
if (config.cloudDiscoveryEnabled && pollingInterval > 0) {
logger.trace("{} starting bridge discovery sheduler", this.uid);
this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval,
TimeUnit.MINUTES);
} else {
stopScheduler(this.discoveryJob);
}
}
/**
* Stop scheduler
*
* @param scheduler ScheduledFeature<?> which schould be stopped
*/
protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
if (scheduler != null) {
scheduler.cancel(true);
scheduler = null;
}
}
/***********************************
*
* ERROR HANDLER
*
************************************/
/**
* return device Error
*
* @return
*/
public TapoErrorHandler getError() {
return this.bridgeError;
}
/**
* set device error
*
* @param tapoError TapoErrorHandler-Object
*/
public void setError(TapoErrorHandler tapoError) {
this.bridgeError.set(tapoError);
}
/***********************************
*
* BRIDGE COMMUNICATIONS
*
************************************/
/**
* Login to Cloud
*
* @return
*/
public boolean loginCloud() {
bridgeError.reset(); // reset ErrorHandler
if (!config.username.isBlank() && !config.password.isBlank()) {
logger.debug("{} login with user {}", this.uid, config.username);
if (cloudConnector.login(config.username, config.password)) {
updateStatus(ThingStatus.ONLINE);
return true;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "credentials not set");
}
return false;
}
/***********************************
*
* DEVICE DISCOVERY
*
************************************/
/**
* START DEVICE DISCOVERY
*/
public void discoverDevices() {
this.discoveryService.startScan();
}
/**
* GET DEVICELIST CONNECTED TO BRIDGE
*
* @return devicelist
*/
public JsonArray getDeviceList() {
JsonArray deviceList = new JsonArray();
if (config.cloudDiscoveryEnabled) {
logger.trace("{} discover devicelist from cloud", this.uid);
deviceList = getDeviceListCloud();
} else if (config.udpDiscoveryEnabled) {
logger.trace("{} discover devicelist from udp", this.uid);
deviceList = getDeviceListUDP();
}
return deviceList;
}
/**
* GET DEVICELIST FROM CLOUD
* returns all devices stored in cloud
*
* @return deviceList from cloud
*/
private JsonArray getDeviceListCloud() {
logger.trace("{} getDeviceList from cloud", this.uid);
bridgeError.reset(); // reset ErrorHandler
JsonArray deviceList = new JsonArray();
if (loginCloud()) {
deviceList = this.cloudConnector.getDeviceList();
}
return deviceList;
}
/**
* GET DEVICELIST UDP
* return devices discovered by UDP
*
* @return deviceList from udp
*/
public JsonArray getDeviceListUDP() {
bridgeError.reset(); // reset ErrorHandler
TapoUDP udpDiscovery = new TapoUDP(credentials);
return udpDiscovery.udpScan();
}
/***********************************
*
* BRIDGE GETTERS
*
************************************/
public TapoCredentials getCredentials() {
return this.credentials;
}
public HttpClient getHttpClient() {
return this.httpClient;
}
public ThingUID getUID() {
return getThing().getUID();
}
public TapoBridgeConfiguration getBridgeConfig() {
return this.config;
}
}

View File

@ -0,0 +1,234 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.device;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.util.HashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TAPO Universal-Device
* universal device for testing pruposes
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoUniversalDevice extends TapoDevice {
private final Logger logger = LoggerFactory.getLogger(TapoUniversalDevice.class);
// CHANNEL LIST
public static final String CHANNEL_GROUP_DEBUG = "debug";
public static final String CHANNEL_RESPONSE = "deviceResponse";
public static final String CHANNEL_COMMAND = "deviceCommand";
/**
* Constructor
*
* @param thing Thing object representing device
*/
public TapoUniversalDevice(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command.toString(), channelUID.getId());
Boolean refreshInfo = false;
String channel = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
refreshInfo = true;
} else {
switch (channel) {
case CHANNEL_OUTPUT:
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, command == OnOffType.ON);
refreshInfo = true;
break;
case CHANNEL_BRIGHTNESS:
if (command instanceof PercentType) {
Float percent = ((PercentType) command).floatValue();
setBrightness(percent.intValue()); // 0..100% = 0..100
refreshInfo = true;
} else if (command instanceof DecimalType) {
setBrightness(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR_TEMP:
if (command instanceof DecimalType) {
setColorTemp(((DecimalType) command).intValue());
refreshInfo = true;
}
break;
case CHANNEL_COLOR:
if (command instanceof HSBType) {
setColor((HSBType) command);
refreshInfo = true;
}
break;
case CHANNEL_COMMAND:
String[] cmd = command.toString().split(":");
if (cmd.length == 1) {
connector.sendCustomQuery(cmd[0]);
} else if (cmd.length == 2) {
connector.sendDeviceCommand(cmd[0], cmd[1]);
} else {
logger.warn("({}) wrong command format '{}'", uid, command.toString());
}
break;
default:
logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
channelUID.getId());
}
}
/* refreshInfo */
if (refreshInfo) {
queryDeviceInfo();
}
}
/**
* SET BRIGHTNESS
*
* @param newBrightness percentage 0-100 of new brightness
*/
protected void setBrightness(Integer newBrightness) {
/* switch off if 0 */
if (newBrightness == 0) {
connector.sendDeviceCommand(DEVICE_PROPERTY_ON, false);
} else {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_BRIGHTNES, newBrightness);
connector.sendDeviceCommands(newState);
}
}
/**
* SET COLOR
*
* @param command
*/
protected void setColor(HSBType command) {
HashMap<String, Object> newState = new HashMap<>();
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_HUE, command.getHue());
newState.put(DEVICE_PROPERTY_SATURATION, command.getSaturation());
newState.put(DEVICE_PROPERTY_BRIGHTNES, command.getBrightness());
connector.sendDeviceCommands(newState);
}
/**
* SET COLORTEMP
*
* @param colorTemp (Integer) in Kelvin
*/
protected void setColorTemp(Integer colorTemp) {
HashMap<String, Object> newState = new HashMap<>();
colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
newState.put(DEVICE_PROPERTY_ON, true);
newState.put(DEVICE_PROPERTY_COLORTEMP, colorTemp);
connector.sendDeviceCommands(newState);
}
/**
* SET DEVICE INFOs to device
*
* @param deviceInfo
*/
@Override
public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
devicePropertiesChanged(deviceInfo);
handleConnectionState();
}
/**
* Handle full responsebody received from connector
*
* @param responseBody
*/
public void responsePasstrough(String responseBody) {
logger.info("({}) received response {}", uid, responseBody);
publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
}
/**
* UPDATE PROPERTIES
*
* @param TapoDeviceInfo
*/
@Override
protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
super.devicePropertiesChanged(deviceInfo);
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
getPercentType(deviceInfo.getBrightness()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
getDecimalType(deviceInfo.getColorTemp()));
publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
getDecimalType(deviceInfo.getSignalLevel()));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
getQuantityType(deviceInfo.getOnTime(), Units.SECOND));
publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
}
/***********************************
*
* CHANNELS
*
************************************/
/**
* Get ChannelID including group
*
* @param group String channel-group
* @param channel String channel-name
* @return String channelID
*/
@Override
protected String getChannelID(String group, String channel) {
return group + "#" + channel;
}
/**
* Get Channel from ChannelID
*
* @param channelID String channelID
* @return String channel-name
*/
protected String getChannelFromID(ChannelUID channelID) {
String channel = channelID.getIdWithoutGroup();
channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
return channel;
}
}

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public final class TapoBridgeConfiguration {
private final Logger logger = LoggerFactory.getLogger(TapoBridgeConfiguration.class);
/* THING CONFIGUTATION PROPERTYS */
public static final String CONFIG_EMAIL = "username";
public static final String CONFIG_PASS = "password";
public static final String CONFIG_DEVICE_IP = "ipAddress";
public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
public static final String CONFIG_CLOUD_UPDATE_INTERVAL = "cloudReconnect";
public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
public static final String CONFIG_DISCOVERY_UDP = "udpDiscovery";
public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
/* thing configuration parameter. */
public String username = "";
public String password = "";
public Boolean cloudDiscoveryEnabled = false;
public Boolean udpDiscoveryEnabled = false;
public Integer cloudReconnectIntervalM = 1440;
public Integer discoveryIntervalM = 30;
private Thing bridge;
/**
* Create settings
*
* @param thing BridgeThing
*/
public TapoBridgeConfiguration(Thing thing) {
this.bridge = thing;
loadSettings();
}
/**
* LOAD SETTINGS
*/
public void loadSettings() {
try {
Configuration config = this.bridge.getConfiguration();
username = config.get(CONFIG_EMAIL).toString();
password = config.get(CONFIG_PASS).toString();
cloudDiscoveryEnabled = Boolean.parseBoolean(config.get(CONFIG_DISCOVERY_CLOUD).toString());
udpDiscoveryEnabled = Boolean.parseBoolean(config.get(CONFIG_DISCOVERY_UDP).toString());
cloudReconnectIntervalM = Integer.valueOf(config.get(CONFIG_CLOUD_UPDATE_INTERVAL).toString());
discoveryIntervalM = Integer.valueOf(config.get(CONFIG_DISCOVERY_INTERVAL).toString());
} catch (Exception e) {
logger.warn("{} error reading configuration: '{}'", bridge.getUID(), e.getMessage());
}
}
}

View File

@ -0,0 +1,242 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.PercentType;
import com.google.gson.JsonObject;
/**
* Tapo-Device Information class
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoDeviceInfo {
/**
* AVAILABLE BUT UNUSED FIELDS
* remove before push to real version
*
* private Boolean hasSetLocationInfo = false;
* private Integer latitude = 0;
* private Integer longitude = 0;
* private Integer timeDiff = 0;
* private String avatar = "";
* private String fwId = "";
* private String hwId = "";
* private String specs = "";
* private String ssid = "";
* private String oemId = "";
* private String lang = "";
* private String location = "";
*/
private Boolean deviceOn = false;
private Boolean overheated = false;
private Integer brightness = 0;
private Integer colorTemp = 0;
private Integer hue = 0;
private Integer rssi = 0;
private Integer saturation = 100;
private Integer signalLevel = 0;
private Number onTime = 0;
private Number timeUsagePast30 = 0;
private Number timeUsagePast7 = 0;
private Number timeUsageToday = 0;
private String deviceId = "";
private String fwVer = "";
private String hwVer = "";
private String ip = "";
private String mac = "";
private String model = "";
private String nickname = "";
private String region = "";
private String type = "";
private TapoLightEffect lightEffect = new TapoLightEffect();
private JsonObject jsonObject = new JsonObject();
/**
* INIT
*/
public TapoDeviceInfo() {
setData();
}
/**
* Init DeviceInfo with new Data;
*
* @param jso JsonObject new Data
*/
public TapoDeviceInfo(JsonObject jso) {
jsonObject = jso;
setData();
}
/**
* Set Data (new JsonObject)
*
* @param jso JsonObject new Data
*/
public TapoDeviceInfo setData(JsonObject jso) {
this.jsonObject = jso;
setData();
return this;
}
private void setData() {
this.brightness = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_BRIGHTNES);
this.colorTemp = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_COLORTEMP, BULB_MIN_COLORTEMP);
this.deviceId = jsonObjectToString(jsonObject, DEVICE_PROPERTY_ID);
this.deviceOn = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_ON);
this.fwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_FW);
this.hue = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_HUE);
this.hwVer = jsonObjectToString(jsonObject, DEVICE_PROPERTY_HW);
this.ip = jsonObjectToString(jsonObject, DEVICE_PROPERTY_IP);
this.lightEffect = lightEffect.setData(jsonObject);
this.mac = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MAC);
this.model = jsonObjectToString(jsonObject, DEVICE_PROPERTY_MODEL);
this.nickname = jsonObjectToString(jsonObject, DEVICE_PROPERTY_NICKNAME);
this.onTime = jsonObjectToNumber(jsonObject, DEVICE_PROPERTY_ONTIME);
this.overheated = jsonObjectToBool(jsonObject, DEVICE_PROPERTY_OVERHEAT);
this.region = jsonObjectToString(jsonObject, DEVICE_PROPERTY_REGION);
this.saturation = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SATURATION);
this.signalLevel = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL);
this.rssi = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_SIGNAL_RSSI);
this.timeUsagePast7 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_7);
this.timeUsagePast30 = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_30);
this.timeUsageToday = jsonObjectToInt(jsonObject, DEVICE_PROPERTY_USAGE_TODAY);
this.type = jsonObjectToString(jsonObject, DEVICE_PROPERTY_TYPE);
}
/***********************************
*
* GET VALUES
*
************************************/
public Integer getBrightness() {
return brightness;
}
public Integer getColorTemp() {
return colorTemp;
}
public String getFirmwareVersion() {
return fwVer;
}
public String getHardwareVersion() {
return hwVer;
}
public HSBType getHSB() {
DecimalType h = new DecimalType(hue);
PercentType s = new PercentType(saturation);
PercentType b = new PercentType(brightness);
return new HSBType(h, s, b);
}
public Integer getHue() {
return hue;
}
public TapoLightEffect getLightEffect() {
return lightEffect;
}
public String getIP() {
return ip;
}
public Boolean isOff() {
return !deviceOn;
}
public Boolean isOn() {
return deviceOn;
}
public Boolean isOverheated() {
return overheated;
}
public String getMAC() {
return formatMac(mac, MAC_DIVISION_CHAR);
}
public String getModel() {
return model.replace(" ", "_");
}
public String getNickname() {
return nickname;
}
public Number getOnTime() {
return onTime;
}
public String getRegion() {
return region;
}
public String getRepresentationProperty() {
return getMAC();
}
public Integer getSaturation() {
return saturation;
}
public String getSerial() {
return deviceId;
}
public Integer getSignalLevel() {
return signalLevel;
}
public Integer getRSSI() {
return rssi;
}
public Number getTimeUsagePast7() {
return timeUsagePast7;
}
public Number getTimeUsagePast30() {
return timeUsagePast30;
}
public Number getTimeUsagePastToday() {
return timeUsageToday;
}
public String getType() {
return type;
}
@Override
public String toString() {
return jsonObject.toString();
}
}

View File

@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.tapocontrol.internal.structures;
import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
import java.awt.Color;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.JsonObject;
/**
* Tapo-LightningEffect Structure Class
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
public class TapoLightEffect {
private Integer enable = 0;
private String id = "";
private String name = "";
private Integer custom = 0;
private Integer brightness = 0;
private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
private Color displayColors[] = { Color.WHITE };
private JsonObject jsonObject = new JsonObject();
/**
* INIT
*/
public TapoLightEffect() {
setData();
}
/**
* Init DeviceInfo with new Data;
*
* @param jso JsonObject new Data
*/
public TapoLightEffect(JsonObject jso) {
setData(jso);
}
/**
* Set Data (new JsonObject)
*
* @param jso JsonObject new Data
*/
public TapoLightEffect setData(JsonObject jso) {
/* create empty jsonObject to set efault values if has no lighning effect */
if (jsonObject.has(DEVICE_PROPERTY_EFFECT)) {
this.jsonObject = jso;
} else {
jsonObject = new JsonObject();
}
setData();
return this;
}
private void setData() {
this.enable = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_ENABLE);
this.id = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_ID);
this.name = jsonObjectToString(jsonObject, PROPERTY_LIGHTNING_EFFECT_NAME);
this.custom = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_CUSTOM); // jsonObjectToBool
this.brightness = jsonObjectToInt(jsonObject, PROPERTY_LIGHTNING_EFFECT_BRIGHNTESS);
// this.color_temp_range = { 9000, 9000 }; PROPERTY_LIGHNTING_ //:[9000,9000]
// this.displayColors[] PROPERTY_LIGHNTING_;
}
/***********************************
*
* SET VALUES
*
************************************/
public void setEnable(Boolean enable) {
this.enable = enable ? 1 : 0;
}
public void setName(String value) {
this.name = value;
}
public void setCustom(Boolean enable) {
this.custom = enable ? 1 : 0;
}
public void setBrightness(Integer value) {
this.brightness = value;
}
public void setColorTempRange() {
}
public void setDisplayColors() {
}
/***********************************
*
* GET VALUES
*
************************************/
public Integer getEnable() {
return this.enable;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public Integer getCustom() {
return this.custom;
}
public Integer getBrightness() {
return this.brightness;
}
public Integer[] getColorTempRange() {
return this.colorTempRange;
}
public Color[] getDisplayColors() {
return this.displayColors;
}
@Override
public String toString() {
return jsonObject.toString();
}
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:tapo:device">
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP Address</label>
</parameter>
<parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
<label>Refresh Interval</label>
<description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
<default>30</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="bridge-type:tapo:bridge">
<parameter name="username" type="text" required="true">
<context>email</context>
<label>Username</label>
<description>Tapo-Cloud Login User (e-Mail)</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
<description>Tapo-Cloud Login Password</description>
</parameter>
<parameter name="cloudDiscovery" type="boolean" required="false">
<label>Cloud Discovery</label>
<description>Use Cloud Discovery-Service</description>
<default>false</default>
<advanced>false</advanced>
</parameter>
<parameter name="cloudReconnect" type="integer" min="0" max="10080" required="false">
<label>Cloud Reconnect Interval</label>
<description>Interval for reconnecting to the Tapo-Cloud in minutes (default 1440 = 24h / 0 = disabled)</description>
<default>1440</default>
<advanced>true</advanced>
</parameter>
<!--
<parameter name="discoveryInterval" type="integer" min="0" max="10080" required="false">
<label>Background Discovery Interval</label>
<description>Interval background discovery in minutes (default 60 / 0 = disabled)</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="udpDiscovery" type="boolean" required="false">
<label>UDP Discovery</label>
<description>Use UDP Discovery-Service</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
-->
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="tapocontrol"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- TEST-DEVICE (Universal) -->
<thing-type id="Test_Device">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Tapo Universal TestDevice</label>
<description>For testing pruposes! Response is written down as info in openhab-log</description>
<channel-groups>
<channel-group id="actuator" typeId="colorBulb"/>
<channel-group id="device" typeId="deviceState"/>
<channel-group id="effect" typeId="lightEffect"/>
<channel-group id="debug" typeId="commandDebug"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
<!-- ############################### CHANNEL-GROUPS ############################### -->
<!-- CHANNEL GROUP TYPES -->
<!--Device-Statuss Channel Type -->
<channel-group-type id="commandDebug">
<label>Device Communication Debug</label>
<description>Device resoponses and command debugging</description>
<channels>
<channel id="deviceResponse" typeId="deviceResponse"/>
<channel id="deviceCommand" typeId="deviceCommand"/>
</channels>
</channel-group-type>
<!-- ############################### CHANNELS ############################### -->
<!-- OuputState Channel Type -->
<channel-type id="deviceResponse">
<item-type>String</item-type>
<label>Device Response</label>
<description>DeviceResponse</description>
<state readOnly="true"/>
</channel-type>
<!-- OuputState Channel Type -->
<channel-type id="deviceCommand">
<item-type>String</item-type>
<label>Device Command</label>
<description>command send to device. use: 'command':'value'</description>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -327,6 +327,7 @@
<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>