mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
35dbde1189
commit
612afd2e07
@ -295,6 +295,7 @@
|
|||||||
/bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis
|
/bundles/org.openhab.binding.tacmi/ @twendt @Wolfgang1966 @marvkis
|
||||||
/bundles/org.openhab.binding.tado/ @dfrommi
|
/bundles/org.openhab.binding.tado/ @dfrommi
|
||||||
/bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag
|
/bundles/org.openhab.binding.tankerkoenig/ @dolic @JueBag
|
||||||
|
/bundles/org.openhab.binding.tapocontrol/ @wildcs
|
||||||
/bundles/org.openhab.binding.telegram/ @ZzetT
|
/bundles/org.openhab.binding.telegram/ @ZzetT
|
||||||
/bundles/org.openhab.binding.teleinfo/ @Nokyyz @olivierkeke
|
/bundles/org.openhab.binding.teleinfo/ @Nokyyz @olivierkeke
|
||||||
/bundles/org.openhab.binding.tellstick/ @openhab/add-ons-maintainers
|
/bundles/org.openhab.binding.tellstick/ @openhab/add-ons-maintainers
|
||||||
|
@ -1466,6 +1466,11 @@
|
|||||||
<artifactId>org.openhab.binding.tankerkoenig</artifactId>
|
<artifactId>org.openhab.binding.tankerkoenig</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.tapocontrol</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.telegram</artifactId>
|
<artifactId>org.openhab.binding.telegram</artifactId>
|
||||||
|
13
bundles/org.openhab.binding.tapocontrol/NOTICE
Normal file
13
bundles/org.openhab.binding.tapocontrol/NOTICE
Normal 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
|
114
bundles/org.openhab.binding.tapocontrol/README.md
Normal file
114
bundles/org.openhab.binding.tapocontrol/README.md
Normal 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" }
|
||||||
|
```
|
15
bundles/org.openhab.binding.tapocontrol/pom.xml
Normal file
15
bundles/org.openhab.binding.tapocontrol/pom.xml
Normal 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>
|
@ -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>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -327,6 +327,7 @@
|
|||||||
<module>org.openhab.binding.tacmi</module>
|
<module>org.openhab.binding.tacmi</module>
|
||||||
<module>org.openhab.binding.tado</module>
|
<module>org.openhab.binding.tado</module>
|
||||||
<module>org.openhab.binding.tankerkoenig</module>
|
<module>org.openhab.binding.tankerkoenig</module>
|
||||||
|
<module>org.openhab.binding.tapocontrol</module>
|
||||||
<module>org.openhab.binding.telegram</module>
|
<module>org.openhab.binding.telegram</module>
|
||||||
<module>org.openhab.binding.teleinfo</module>
|
<module>org.openhab.binding.teleinfo</module>
|
||||||
<module>org.openhab.binding.tellstick</module>
|
<module>org.openhab.binding.tellstick</module>
|
||||||
|
Loading…
Reference in New Issue
Block a user