The following Tapo-Devices are supported. For precise channel-description look at `channels-table` below
-| DeviceType | ThingType | Description |
-|------------------------------------|-------------|---------------------------------------------|
-| SmartPlug (Wi-Fi) | P100 | Smart Socket |
-| | P105 | Smart Mini Socket |
-| EnergyMonitoring SmartPlug (Wi-Fi) | P110 | Energy Monitoring Smart Socket |
-| | P115 | Energy Monitoring Mini Smart Socket |
-| Power Strip (Wi-Fi) | P300 | Smart Wi-Fi Power Strip - 3 sockets |
-| Dimmable SmartBulb (Wi-Fi) | L510 | Dimmable White-Light Smart-Bulb (E27) |
-| | L610 | Dimmable White-Light Smart-Spot (GU10) |
-| MultiColor SmartBulb (Wi-Fi) | L530 | Multicolor Smart-Bulb (E27) |
-| | L630 | Multicolor Smart-Spot (GU10) |
-| MultiColor LightStrip (Wi-Fi) | L900 | Multicolor RGB Dimmable LightStrip (5m) |
-| | L920 | Multicolor RGB-IC ColorZone LightStrip (5m) |
-| | L930 | Multicolor RGBW-IC 50-Zone LightStrip (5m) |
+| DeviceType | ThingType | Description |
+|------------------------------------|-------------|----------------------------------------------|
+| SmartPlug (Wi-Fi) | P100 | Smart Socket |
+| | P105 | Smart Mini Socket |
+| EnergyMonitoring SmartPlug (Wi-Fi) | P110 | Energy Monitoring Smart Socket |
+| | P115 | Energy Monitoring Mini Smart Socket |
+| Power Strip (Wi-Fi) | P300 | Smart Wi-Fi Power Strip - 3 sockets |
+| Dimmable SmartBulb (Wi-Fi) | L510 | Dimmable White-Light Smart-Bulb (E27) |
+| | L610 | Dimmable White-Light Smart-Spot (GU10) |
+| MultiColor SmartBulb (Wi-Fi) | L530 | Multicolor Smart-Bulb (E27) |
+| | L630 | Multicolor Smart-Spot (GU10) |
+| MultiColor LightStrip (Wi-Fi) | L900 | Multicolor RGB Dimmable LightStrip (5m) |
+| | L920 | Multicolor RGB-IC ColorZone LightStrip (5m) |
+| | L930 | Multicolor RGBW-IC 50-Zone LightStrip (5m) |
+| Smart Hub (Wi-Fi / RF) | H100 | Smart Hub with Chime to control Child Devices|
+| Smart Contact Sensor (RF) | T110 | Window/Door Smart Contact Sensor |
+| Smart Temperature Sensor (RF) | T310 | Temperature and Humidity Sensor |
+| | T315 | Temperature and Humidity Sensor with Display |
## Prerequisites
## Discovery
-Discovery is done by connecting to the Tapo-Cloud Service.
-All devices stored in your cloud account will be detected even if they are not in your network.
-You need to know the IP-Adress of your device. This must be set manually in the thing configuration
+For Wi-Fi-Devices, discovery is done by connecting to the Tapo-Cloud-Service or use local udp-discovery.
+If enabled, all devices stored in your cloud account will be detected even if they are not in your local network.
+From cloud you can get more informations such as "Device-Alias" as from udp-discovery.
+But you need to know the IP-Adress of your device. This must be set manually in the thing configuration.
+
+UDP-Discovery can find only devices which are online in your local network and get less informations as from cloud.
+But therefore it set's device-ip and protocol automaticly.
+If you have problems with udp-discovery, try to set the advanced setting 'broadcastAddress' to your local subnet ('e.g. 192.168.0.255').
+Default is '255.255.255.255'
+
+You can combine both discovery methods to get any informations from local devices.
+If you enable setting 'onlyLocalOnlineDevices' results will only generated for local online devices but with the combined data of cloud discovery.
+RF-Devices will be discovered by the hub they are connected to.
+You can discover them manually or use ´backgroundDiscovery´
+
+RF-Devices will be discovered by the hub they are connected to. You can discover them manually or use ´backgroundDiscovery´
## Bridge Configuration
The thing has the following configuration parameters:
-| Parameter | Description |
-|--------------------|----------------------------------------------------------------------|
-| username | Username (eMail) of your Tapo-Cloud |
-| password | Password of your Tapo-Cloud |
+| Parameter | Description |
+|------------------------|----------------------------------------------------------------------------------------------------------------|
+| username | Username (eMail) of your Tapo-Cloud |
+| password | Password of your Tapo-Cloud |
+| cloudDiscovery | Use Cloud Discovery-Service to get all in Tapo-App registered devices. Includes DeviceName. IP-Address and Encryption has to set manually |
+| udpDiscovery | Use UDP Discovery-Service to discover online devices in the local network. Includes Encryption and IP-Address. Results will be merged with cloud discovery |
+| onlyLocalOnlineDevices | [advanced] Uses Cloud and UPD-Discovery to get more informations but will only discover online devices via UDP |
+| broadcastAddress | [advanced] Set broadcast address to your local subnet if you have problems with default address |
+| discoveryInterval | [advanced] Interval in minutes when a background device scan should be executed. Default is 60 |
+
## Thing Configuration
-The thing needs to be configured with `ipAddress`.
+WiFi-based things needs to be configured with `ipAddress`.
+RF-based things need a SmartHub(WiFi-Device) to operate.
-The thing has the following configuration parameters:
+The things has the following configuration parameters:
+
+| Parameter | Description | Things supporting parameter |
+|--------------------|-----------------------------------------------------------------------|-----------------------------|
+| ipAddress | IP Address of the device. | Any Wi-Fi-Device |
+| pollingInterval | [optional] Refresh interval in seconds. The default is 30 seconds | Any Wi-Fi-Device |
+| httpPort | [optional] HTTP-Communication Port. Default is 80 | Any Wi-Fi-Device |
+| protocol | [optional] Used Communication Protocol (AES/KLAP/'') Default 'AES' | Any Wi-Fi-Device |
+| backgroundDiscovery| [optional] RF-Devices will be discovered after every polling request | SmartHub |
-| Parameter | Description |
-|--------------------|----------------------------------------------------------------------|
-| ipAddress | IP Address of the device. |
-| pollingInterval | Refresh interval in seconds. Optional. The default is 30 seconds |
## Channels
| | brightness | Dimmer | Brightness 0-100% | L510, L530, L610, L630, L900, L920 |
| | colorTemperature | Number | White-Color-Temp 2500-6500K | L510, L530, L610, L630, L900, L920 |
| | color | Color | Color | L530, L630, L900, L920 |
-| effects | fxName | String | Active lightning effect | L530 |
+| sensor | isOpen | Switch | Contact (Door/Window) is Open | T110 |
+| | currentTemp | Number:Temperature | Current Temperature | T310, T315 |
+| | currentHumidity | Number:Dimensionless | Current relative humidity in % | T310, T315 |
+| effects | fxName | String | Active lightning effect | L530, L900, L920, L930 |
| device | wifiSignal | Number | WiFi-quality-level | P100, P105, P110, P115, L510, L530, L610, L630, L900, L920, L930 |
| | onTime | Number:Time | seconds output is on | P100, P105, P110, P115, L510, L530, L900, L920, L930 |
+| | signalStrength | Number | RF-quality-level | T110 |
+| | isOnline | Switch | Device is Online | T110 |
+| | batteryLow | Switch | Battery of device is low | T110 |
| energy | actualPower | Number:Power | actual Power (Watt) | P110, P115 |
| | todayEnergyUsage | Number:Energy | used energy today (Wh) | P110, P115 |
| | todayRuntime | Number:Time | seconds output was on today | P110, P115 |
+| alarm | alarmActive | Switch | Alarm is currntly active | H100 |
+| | alarmSource | String | Source causes active alarm | H100 |
## Channel Refresh
```java
tapocontrol:bridge:myTapoBridge "Cloud-Login" [ username="you@yourpovider.com", password="verysecret" ]
-tapocontrol:P100:myTapoBridge:mySocket "My-Socket" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.150", pollingInterval=30 ]
-tapocontrol:L510:myTapoBridge:whiteBulb "white-light" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.151", pollingInterval=30 ]
-tapocontrol:L530:myTapoBridge:colorBulb "color-light" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.152", pollingInterval=30 ]
-tapocontrol:L900:myTapoBridge:myLightStrip "light-strip" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.153", pollingInterval=30 ]
+tapocontrol:P100:myTapoBridge:mySocket "My-Socket" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.150" ]
+tapocontrol:L510:myTapoBridge:whiteBulb "white-light" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.151", httpPort=80, pollingInterval=30, protocol="AES" ]
+tapocontrol:L530:myTapoBridge:colorBulb "color-light" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.152", pollingInterval=30, protocol="KLAP" ]
+tapocontrol:L900:myTapoBridge:myLightStrip "light-strip" (tapocontrol:bridge:myTapoBridge) [ ipAddress="192.168.178.153", pollingInterval=30, protocol="" ]
Bridge tapocontrol:bridge:secondBridgeExample "Cloud-Login" [ username="youtoo@anyprovider.com", password="verysecret" ] {
Thing P110 mySocket "My-Socket" [ ipAddress="192.168.101.51", pollingInterval=30 ]
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.device.TapoLightStrip;
-import org.openhab.binding.tapocontrol.internal.device.TapoSmartBulb;
-import org.openhab.binding.tapocontrol.internal.device.TapoSmartPlug;
-import org.openhab.binding.tapocontrol.internal.device.TapoUniversalDevice;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.rf.smartcontact.TapoSmartContactHandler;
+import org.openhab.binding.tapocontrol.internal.devices.rf.wheatersensor.TapoWheaterSensorHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoUniversalDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.bulb.TapoBulbHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.hub.TapoHubHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.lightstrip.TapoLightStripHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.socket.TapoSocketHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.socket.TapoSocketStripHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
/**
- * The {@link ThingHandler} is responsible for handling commands, which are
+ * The {@link TapoControlHandlerFactory} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Christian Wild - Initial contribution
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.tapocontrol")
@NonNullByDefault
public class TapoControlHandlerFactory extends BaseThingHandlerFactory {
+ public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation()
+ .create();
private final Logger logger = LoggerFactory.getLogger(TapoControlHandlerFactory.class);
private final Set<TapoBridgeHandler> accountHandlers = new HashSet<>();
private final HttpClient httpClient;
TapoBridgeHandler bridgeHandler = new TapoBridgeHandler((Bridge) thing, httpClient);
accountHandlers.add(bridgeHandler);
return bridgeHandler;
- } else if (SUPPORTED_SMART_PLUG_UIDS.contains(thingTypeUID)) {
- return new TapoSmartPlug(thing);
+ } else if (SUPPORTED_HUB_UIDS.contains(thingTypeUID)) {
+ return new TapoHubHandler(thing);
+ } else if (SUPPORTED_SOCKET_UIDS.contains(thingTypeUID)) {
+ return new TapoSocketHandler(thing);
+ } else if (SUPPORTED_SOCKET_STRIP_UIDS.contains(thingTypeUID)) {
+ return new TapoSocketStripHandler(thing);
} else if (SUPPORTED_WHITE_BULB_UIDS.contains(thingTypeUID)) {
- return new TapoSmartBulb(thing);
+ return new TapoBulbHandler(thing);
} else if (SUPPORTED_COLOR_BULB_UIDS.contains(thingTypeUID)) {
- return new TapoSmartBulb(thing);
+ return new TapoBulbHandler(thing);
} else if (SUPPORTED_LIGHT_STRIP_UIDS.contains(thingTypeUID)) {
- return new TapoLightStrip(thing);
+ return new TapoLightStripHandler(thing);
+ } else if (SUPPORTED_SMART_CONTACTS.contains(thingTypeUID)) {
+ return new TapoSmartContactHandler(thing);
+ } else if (SUPPORTED_WHEATHER_SENSORS.contains(thingTypeUID)) {
+ return new TapoWheaterSensorHandler(thing);
} else if (thingTypeUID.equals(UNIVERSAL_THING_TYPE)) {
- return new TapoUniversalDevice(thing);
+ return new TapoUniversalDeviceHandler(thing);
}
return null;
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoBridgeConfiguration;
-import org.openhab.core.config.discovery.AbstractThingHandlerDiscoveryService;
-import org.openhab.core.config.discovery.DiscoveryResult;
-import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.config.discovery.DiscoveryService;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-import org.osgi.service.component.annotations.Component;
-import org.osgi.service.component.annotations.ServiceScope;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home thing discovery
- *
- * @author Christian Wild - Initial contribution
- */
-@Component(scope = ServiceScope.PROTOTYPE, service = TapoDiscoveryService.class)
-@NonNullByDefault
-public class TapoDiscoveryService extends AbstractThingHandlerDiscoveryService<TapoBridgeHandler> {
- private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
-
- /***********************************
- *
- * INITIALIZATION
- *
- ************************************/
-
- /**
- * INIT CLASS
- */
- public TapoDiscoveryService() {
- super(TapoBridgeHandler.class, SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
- }
-
- @Override
- public void initialize() {
- thingHandler.setDiscoveryService(this);
- TapoBridgeConfiguration config = thingHandler.getBridgeConfig();
- modified(Map.of(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, config.cloudDiscovery));
- super.initialize();
- }
-
- /***********************************
- *
- * SCAN HANDLING
- *
- ************************************/
-
- /**
- * Start scan manually
- */
- @Override
- public void startScan() {
- removeOlderResults(getTimestampOfLastScan());
- JsonArray jsonArray = thingHandler.getDeviceList();
- handleCloudDevices(jsonArray);
- }
-
- /***********************************
- *
- * handle Results
- *
- ************************************/
-
- /**
- * CREATE DISCOVERY RESULT
- * creates discoveryResult (Thing) from JsonObject got from Cloud
- *
- * @param device JsonObject with device information
- * @return DiscoveryResult-Object
- */
- public DiscoveryResult createResult(JsonObject device) {
- String deviceModel = getDeviceModel(device);
- String label = getDeviceLabel(device);
- String deviceMAC = device.get(CLOUD_JSON_KEY_MAC).getAsString();
- ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
- /* create properties */
- Map<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_JSON_KEY_FW).getAsString());
- properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.get(CLOUD_JSON_KEY_HW).getAsString());
- properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
- properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.get(CLOUD_JSON_KEY_ID).getAsString());
-
- logger.debug("device {} discovered", deviceModel);
- ThingUID bridgeUID = thingHandler.getUID();
- ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
- return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
- .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
- .build();
- }
-
- /**
- * work with result from get devices from cloud devices
- *
- * @param deviceList
- */
- protected void handleCloudDevices(JsonArray deviceList) {
- try {
- for (JsonElement deviceElement : deviceList) {
- if (deviceElement.isJsonObject()) {
- JsonObject device = deviceElement.getAsJsonObject();
- String deviceModel = getDeviceModel(device);
- ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
- /* create thing */
- if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
- DiscoveryResult discoveryResult = createResult(device);
- thingDiscovered(discoveryResult);
- }
- }
- }
- } catch (Exception e) {
- logger.debug("error handling CloudDevices", e);
- }
- }
-
- /**
- * GET DEVICEMODEL
- *
- * @param device JsonObject with deviceData
- * @return String with DeviceModel
- */
- protected String getDeviceModel(JsonObject device) {
- try {
- String deviceModel = device.get(CLOUD_JSON_KEY_MODEL).getAsString();
- deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
- deviceModel = deviceModel.replace("Tapo", "");
- deviceModel = deviceModel.replace("Series", "");
- deviceModel = deviceModel.trim();
- deviceModel = deviceModel.replace(" ", "_");
- return deviceModel;
- } catch (Exception e) {
- logger.debug("error getDeviceModel", e);
- return "";
- }
- }
-
- /**
- * GET DEVICE LABEL
- *
- * @param device JsonObject with deviceData
- * @return String with DeviceLabel
- */
- protected String getDeviceLabel(JsonObject device) {
- try {
- String deviceLabel = "";
- String deviceModel = getDeviceModel(device);
- ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
- if (SUPPORTED_SMART_PLUG_UIDS.contains(deviceUID)) {
- deviceLabel = DEVICE_DESCRIPTION_SMART_PLUG;
- } else if (SUPPORTED_WHITE_BULB_UIDS.contains(deviceUID)) {
- deviceLabel = DEVICE_DESCRIPTION_WHITE_BULB;
- } else if (SUPPORTED_COLOR_BULB_UIDS.contains(deviceUID)) {
- deviceLabel = DEVICE_DESCRIPTION_COLOR_BULB;
- }
- return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
- } catch (Exception e) {
- logger.debug("error getDeviceLabel", e);
- return "";
- }
- }
-}
package org.openhab.binding.tapocontrol.internal.api;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.util.StringContentProvider;
-import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
+import org.openhab.binding.tapocontrol.internal.api.protocol.passthrough.PassthroughProtocol;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.dto.TapoCloudLoginData;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.dto.TapoCloudLoginResult;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResultList;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.gson.Gson;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-
/**
* Handler class for TAPO-Cloud connections.
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
-public class TapoCloudConnector {
+public class TapoCloudConnector implements TapoConnectorInterface {
private final Logger logger = LoggerFactory.getLogger(TapoCloudConnector.class);
private final TapoBridgeHandler bridge;
- private final Gson gson = new Gson();
- private final HttpClient httpClient;
+ private @NonNullByDefault({}) TapoCloudLoginResult loginResult;
private String token = "";
- private String url = TAPO_CLOUD_URL;
private String uid;
+ private PassthroughProtocol passthrough;
- /**
- * INIT CLASS
- *
- */
- public TapoCloudConnector(TapoBridgeHandler bridge, HttpClient httpClient) {
+ /***********************
+ * Init Class
+ **********************/
+
+ public TapoCloudConnector(TapoBridgeHandler bridge) {
this.bridge = bridge;
- this.httpClient = httpClient;
this.uid = bridge.getUID().getAsString();
+ passthrough = new PassthroughProtocol(this);
}
+ /***********************
+ * Response-Handling
+ **********************/
+
/**
- * handle error
- *
- * @param tapoError TapoErrorHandler
+ * handle received reponse-string
*/
- protected void handleError(TapoErrorHandler tapoError) {
- this.bridge.setError(tapoError);
+ @Override
+ public void responsePasstrough(String response, String command) {
}
- /***********************************
- *
- * HTTP (Cloud)-Actions
- *
- ************************************/
-
/**
- * LOGIN TO CLOUD (get Token)
- *
- * @param username unencrypted username
- * @param password unencrypted password
- * @return true if login was successfull
+ * handle received response
*/
- public Boolean login(String username, String password) {
- this.token = getToken(username, password, UUID.randomUUID().toString());
- this.url = TAPO_CLOUD_URL + "?token=" + token;
- return !this.token.isBlank();
+ @Override
+ public void handleResponse(TapoResponse tapoResponse, String command) throws TapoErrorHandler {
+ switch (command) {
+ case CLOUD_CMD_LOGIN:
+ handleLoginResult(tapoResponse);
+ break;
+ case CLOUD_CMD_GETDEVICES:
+ bridge.getDiscoveryService().addScanResults(getDeviceListFromResponse(tapoResponse));
+ break;
+ default:
+ logger.debug("({}) handleResponse - unknown command: {}", uid, command);
+ throw new TapoErrorHandler(ERR_BINDING_NOT_IMPLEMENTED);
+ }
}
/**
- * logout
+ * set bridge error
*/
- public void logout() {
- this.token = "";
+ @Override
+ public void handleError(TapoErrorHandler tapoError) {
+ bridge.setError(tapoError);
}
+ /***********************
+ * Login Handling
+ **********************/
/**
- * GET TOKEN FROM TAPO-CLOUD
- *
- * @param email
- * @param password
- * @param terminalUUID
- * @return
+ * login to cloud
*/
- private String getToken(String email, String password, String terminalUUID) {
- String token = "";
-
- /* create login payload */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = "login";
- plBuilder.addParameter("appType", TAPO_APP_TYPE);
- plBuilder.addParameter("cloudUserName", email);
- plBuilder.addParameter("cloudPassword", password);
- plBuilder.addParameter("terminalUUID", terminalUUID);
- String payload = plBuilder.getPayload();
-
- ContentResponse response = sendCloudRequest(TAPO_CLOUD_URL, payload);
- if (response != null) {
- token = getTokenFromResponse(response);
- }
- return token;
+ public boolean login(TapoCredentials credentials) throws TapoErrorHandler {
+ logout();
+
+ TapoCloudLoginData loginData = new TapoCloudLoginData(credentials.username(), credentials.password());
+ TapoRequest request = new TapoRequest(CLOUD_CMD_LOGIN, loginData);
+
+ passthrough.sendRequest(request);
+ return isLoggedIn();
}
- private String getTokenFromResponse(ContentResponse response) {
- /* work with response */
- if (response.getStatus() == 200) {
- String rBody = response.getContentAsString();
- JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class);
- if (jsonObject != null) {
- Integer errorCode = jsonObject.get("error_code").getAsInt();
- if (errorCode == 0) {
- token = jsonObject.getAsJsonObject("result").get("token").getAsString();
- } else {
- /* return errorcode from device */
- String msg = jsonObject.get("msg").getAsString();
- handleError(new TapoErrorHandler(errorCode, msg));
- logger.trace("cloud returns error: '{}'", rBody);
- }
- } else {
- handleError(new TapoErrorHandler(ERR_API_JSON_DECODE_FAIL));
- logger.trace("unexpected json-response '{}'", rBody);
- }
- } else {
- handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
- logger.warn("invalid response while login");
- token = "";
+ /*
+ * get response from login and set token
+ */
+ private void handleLoginResult(TapoResponse tapoResponse) throws TapoErrorHandler {
+ logger.trace("({}) received login result: {}", uid, tapoResponse);
+ loginResult = getObjectFromJson(tapoResponse.result(), TapoCloudLoginResult.class);
+ token = loginResult.token();
+ if (!isLoggedIn()) {
+ throw new TapoErrorHandler(ERR_API_LOGIN);
}
- return token;
}
+ public void logout() {
+ loginResult = new TapoCloudLoginResult();
+ token = "";
+ }
+
+ public boolean isLoggedIn() {
+ return !token.isBlank();
+ }
+
+ /***********************
+ * Cloud Action Handlers
+ **********************/
/**
- *
- * @return JsonArray with deviceList
+ * Query DeviceList from cloud
*/
- public JsonArray getDeviceList() {
- /* create payload */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = "getDeviceList";
- String payload = plBuilder.getPayload();
-
- ContentResponse response = sendCloudRequest(this.url, payload);
- if (response != null) {
- return getDeviceListFromResponse(response);
+ public void getDeviceList() {
+ TapoRequest request = new TapoRequest(CLOUD_CMD_GETDEVICES);
+ logger.trace("({}) sending cloud command: {}", uid, request);
+ try {
+ passthrough.sendRequest(request);
+ } catch (TapoErrorHandler tapoError) {
+ logger.debug("({}) get devicelist failed: {}", uid, tapoError.getCode());
+ handleError(tapoError);
}
- return new JsonArray();
}
/**
- * get DeviceList from Contenresponse
- *
- * @param response
- * @return
+ * get DeviceList from response
*/
- private JsonArray getDeviceListFromResponse(ContentResponse response) {
- /* work with response */
- if (response.getStatus() == 200) {
- String rBody = response.getContentAsString();
- JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class);
- if (jsonObject != null) {
- /* get errocode (0=success) */
- Integer errorCode = jsonObject.get("error_code").getAsInt();
- if (errorCode == 0) {
- JsonObject result = jsonObject.getAsJsonObject("result");
- return result.getAsJsonArray("deviceList");
- } else {
- /* return errorcode from device */
- handleError(new TapoErrorHandler(errorCode, "device answers with errorcode"));
- logger.trace("cloud returns error: '{}'", rBody);
- }
- } else {
- logger.trace("enexpected json-response '{}'", rBody);
- }
- } else {
- logger.trace("response error '{}'", response.getContentAsString());
- }
- return new JsonArray();
+ private TapoDiscoveryResultList getDeviceListFromResponse(TapoResponse tapoResponse) throws TapoErrorHandler {
+ logger.trace("({}) received devicelist: {}", uid, tapoResponse);
+ return getObjectFromJson(tapoResponse.result(), TapoDiscoveryResultList.class);
}
- /***********************************
- *
- * HTTP-ACTIONS
- *
- ************************************/
+ /***********************
+ * Get Values
+ **********************/
+
+ @Override
+ public HttpClient getHttpClient() {
+ return bridge.getHttpClient();
+ }
+
+ @Override
+ public String getThingUID() {
+ return bridge.getUID().toString();
+ }
+
+ /************************
+ * Private Helpers
+ ************************/
+
/**
- * SEND SYNCHRON HTTP-REQUEST
- *
- * @param url url request is sent to
- * @param payload payload (String) to send
- * @return ContentResponse of request
+ * Get Cloud-URL
*/
- @Nullable
- protected ContentResponse sendCloudRequest(String url, String payload) {
- Request httpRequest = httpClient.newRequest(url).method(HttpMethod.POST.toString());
- httpRequest.timeout(TAPO_HTTP_CLOUD_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ @Override
+ public String getBaseUrl() {
+ String url = TAPO_CLOUD_URL;
+ if (!token.isBlank()) {
+ url = url + "?token=" + token;
+ }
+ return url;
+ }
- /* set header */
+ /**
+ * Set http-headers
+ */
+ public Request setHeaders(Request httpRequest) {
httpRequest.header("content-type", CONTENT_TYPE_JSON);
httpRequest.header("Accept", CONTENT_TYPE_JSON);
-
- /* add request body */
- httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
-
- try {
- return httpRequest.send();
- } catch (InterruptedException e) {
- logger.debug("({}) sending request interrupted: {}", uid, e.toString());
- handleError(new TapoErrorHandler(e));
- } catch (TimeoutException e) {
- logger.debug("({}) sending request timeout: {}", uid, e.toString());
- handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, e.toString()));
- } catch (Exception e) {
- logger.debug("({}) sending request failed: {}", uid, e.toString());
- handleError(new TapoErrorHandler(e));
- }
- return null;
+ return httpRequest;
}
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * Interface for TAPO-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public interface TapoConnectorInterface {
+
+ /* get http-client from bridge */
+ HttpClient getHttpClient();
+
+ /* handle received taporesponse */
+ public void handleResponse(TapoResponse tapoResponse, String command) throws TapoErrorHandler;
+
+ /* handle error */
+ public void handleError(TapoErrorHandler e);
+
+ /* handle received reponse-string */
+ public void responsePasstrough(String response, String command);
+
+ /* get base url of device */
+ public String getBaseUrl();
+
+ /* geth ThingUID of device */
+ public String getThingUID();
+}
*/
package org.openhab.binding.tapocontrol.internal.api;
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.jsonObjectToInt;
import java.net.InetAddress;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
-import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
-import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolEnum;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
+import org.openhab.binding.tapocontrol.internal.api.protocol.aes.SecurePassthrough;
+import org.openhab.binding.tapocontrol.internal.api.protocol.klap.KlapProtocol;
+import org.openhab.binding.tapocontrol.internal.api.protocol.passthrough.PassthroughProtocol;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoChildRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoMultipleRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoChild;
-import org.openhab.binding.tapocontrol.internal.structures.TapoChildData;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoEnergyData;
-import org.openhab.binding.tapocontrol.internal.structures.TapoSubRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
/**
- * Handler class for TAPO Smart Home device connections.
+ * Connection handler class for TAPO wifi devices.
* This class uses asynchronous HttpClient-Requests
*
* @author Christian Wild - Initial contribution
*/
@NonNullByDefault
-public class TapoDeviceConnector extends TapoDeviceHttpApi {
-
+public class TapoDeviceConnector implements TapoConnectorInterface {
private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
-
- private TapoDeviceInfo deviceInfo = new TapoDeviceInfo();
- private TapoEnergyData energyData = new TapoEnergyData();
- private TapoChildData childData = new TapoChildData();
+ private final TapoBaseDeviceHandler device;
+ private final TapoBridgeHandler bridge;
+ private final String uid;
+ private TapoProtocolInterface protocolHandler;
+ private TapoResponse queryResponse = new TapoResponse();
private long lastQuery = 0L;
private long lastSent = 0L;
private long lastLogin = 0L;
+ private boolean queryAfterCommand = false;
+
+ /***********************
+ * Init Class
+ **********************/
+
+ public TapoDeviceConnector(TapoBaseDeviceHandler tapoDeviceHandler, TapoBridgeHandler bridgeThingHandler) {
+ bridge = bridgeThingHandler;
+ device = tapoDeviceHandler;
+ uid = device.getThingUID().toString() + " / DeviceConnector";
+ protocolHandler = setProtocol(device.getDeviceConfig().protocol);
+ }
/**
- * INIT CLASS
- *
- * @param device
- * @param bridgeThingHandler
+ * Set DeviceProtocol which is used for communication
*/
- public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
- super(device, bridgeThingHandler);
+ protected TapoProtocolInterface setProtocol(String protocol) {
+ switch (TapoProtocolEnum.valueOfString(protocol)) {
+ case PASSTHROUGH:
+ logger.trace("({}) selected passtrough-protocol '{}'", uid, protocol);
+ return new PassthroughProtocol(this);
+ case SECUREPASSTROUGH:
+ logger.trace("({}) selected secure-passtrough-protocol '{}'", uid, protocol);
+ return new SecurePassthrough(this);
+ case KLAP:
+ logger.trace("({}) selected klap-protocol '{}'", uid, protocol);
+ return new KlapProtocol(this);
+ default:
+ logger.debug("({}) unknown protocol '{}'", uid, protocol);
+ handleError(new TapoErrorHandler(NO_ERROR, protocol));
+ return new PassthroughProtocol(this);
+ }
}
- /***********************************
- *
+ /***********************
* LOGIN FUNCTIONS
- *
- ************************************/
- /**
- * login
- *
- * @return true if success
- */
- public boolean login() {
- if (this.pingDevice()) {
- logger.trace("({}) sending login to url '{}'", uid, deviceURL);
+ **********************/
+ public boolean login() {
+ if (pingDevice()) {
long now = System.currentTimeMillis();
- if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
- this.lastLogin = now;
- unsetToken();
- unsetCookie();
-
- /* create ssl-handschake (cookie) */
- String cookie = createHandshake();
- if (!cookie.isBlank()) {
- setCookie(cookie);
- String token = queryToken();
- setToken(token);
+ if (now > lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
+ lastLogin = now;
+ try {
+ protocolHandler.login(bridge.getCredentials());
+ } catch (TapoErrorHandler tapoError) {
+ logger.debug("({}) exception while login '{}'", uid, tapoError.toString());
+ handleError(tapoError);
}
- } else {
- logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
}
- return this.loggedIn();
} else {
- logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
+ logger.debug("({}) no ping while login '{}'", uid, device.getIpAddress());
handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE, "no ping while login"));
- return false;
}
+ return protocolHandler.isLoggedIn();
}
- /***********************************
- *
+ public void logout() {
+ protocolHandler.logout();
+ }
+
+ public boolean isLoggedIn() {
+ return protocolHandler.isLoggedIn();
+ }
+
+ /***********************
* DEVICE ACTIONS
- *
- ************************************/
+ **********************/
/**
- * send custom command to device
+ * Send raw (unsecured) request to device
+ */
+ public void sendRawCommand(TapoRequest request) {
+ try {
+ PassthroughProtocol passtrhough = new PassthroughProtocol(this);
+ passtrhough.sendAsyncRequest(request);
+ } catch (TapoErrorHandler tapoError) {
+ logger.debug("({}) send raw command failed '{}'", uid, tapoError.toString());
+ handleError(tapoError);
+ }
+ }
+
+ /**
+ * Send DeviceQueryCommand to Device
*
- * @param queryMethod query method
+ * @param queryCommand Command to be queried
*/
- public void sendCustomQuery(String queryMethod) {
- /* create payload */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = queryMethod;
- sendCustomPayload(plBuilder);
+ public void sendQueryCommand(String queryCommand) {
+ sendQueryCommand(queryCommand, false);
}
/**
- * send custom command to device
+ * Send Custom DeviceQuery
*
- * @param plBuilder Payloadbuilder with unencrypted payload
+ * @param queryCommand Command to be queried
+ * @param ignoreGap ignore gap to last query. query anyway
*/
- public void sendCustomPayload(PayloadBuilder plBuilder) {
+ public void sendQueryCommand(String queryCommand, boolean ignoreGap) {
+ queryAfterCommand = false;
long now = System.currentTimeMillis();
- if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
- String payload = plBuilder.getPayload();
- sendSecurePasstrhroug(payload, DEVICE_CMD_CUSTOM);
+ if (ignoreGap || now > lastQuery + TAPO_QUERY_MIN_GAP_MS) {
+ lastQuery = now;
+ sendAsyncRequest(new TapoRequest(queryCommand));
} else {
- logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+ logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastQuery);
}
}
/**
- * send "set_device_info" command to device
- *
- * @param name Name of command to send
- * @param value Value to send to control
+ * Send "set_device_info" command to device and query info immediately
+ *
+ * @param deviceDataClass clazz contains devicedata which should be sent
*/
- public void sendDeviceCommand(String name, Object value) {
- sendDeviceCommand(DEVICE_CMD_SETINFO, name, value);
+ public void sendDeviceCommand(Object deviceDataClass) {
+ sendDeviceCommand(DEVICE_CMD_SETINFO, deviceDataClass);
}
/**
- * send "set_device_info" command to device
- *
- * @param method Method command belongs to
- * @param name Name of command to send
- * @param value Value to send to control
+ * Send command to device with params and query info immediately
+ *
+ * @param command command
+ * @param deviceDataClass clazz contains devicedata which should be sent
*/
- public void sendDeviceCommand(String method, String name, Object value) {
- long now = System.currentTimeMillis();
- if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
- this.lastSent = now;
-
- /* create payload */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = method;
- plBuilder.addParameter(name, value);
- String payload = plBuilder.getPayload();
-
- sendSecurePasstrhroug(payload, method);
- } else {
- logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
- }
+ public void sendDeviceCommand(String command, Object deviceDataClass) {
+ sendDeviceCommand(command, deviceDataClass, false);
}
/**
- * send "set_device_info" command to child's device
- *
- * @param index of the child
- * @param childProperty to modify
- * @param value for the property
+ * Send command to device with params
+ *
+ * @param command command
+ * @param deviceDataClass clazz contains devicedata which should be sent
+ * @param ignoreGap ignore gap to last query. query anyway
*/
- public void sendChildCommand(Integer index, String childProperty, Object value) {
+ public void sendDeviceCommand(String command, Object deviceDataClass, boolean ignoreGap) {
+ queryAfterCommand = false;
long now = System.currentTimeMillis();
- if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
- this.lastSent = now;
- getChild(index).ifPresent(child -> {
- child.setDeviceOn(Boolean.valueOf((Boolean) value));
- TapoSubRequest request = new TapoSubRequest(child.getDeviceId(), DEVICE_CMD_SETINFO, child);
- sendSecurePasstrhroug(GSON.toJson(request), request.method());
- });
+ if (ignoreGap || now > lastSent + TAPO_SEND_MIN_GAP_MS) {
+ sendAsyncRequest(new TapoRequest(command, deviceDataClass));
} else {
- logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
+ logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastSent);
}
}
/**
- * send multiple "set_device_info" commands to device
- *
- * @param map {@code HashMap<String, Object> (name, value of parameter)}
+ * Send command to device with params and query info immediately
+ *
+ * @param deviceDataClass clazz contains devicedata which should be sent
+ * @param multipleRequestSupported set to true if device supports multipleRequests
*/
- public void sendDeviceCommands(HashMap<String, Object> map) {
- sendDeviceCommands(DEVICE_CMD_SETINFO, map);
+ public void sendCommandAndQuery(Object deviceDataClass, boolean multipleRequestSupported) {
+ sendCommandAndQuery(DEVICE_CMD_SETINFO, deviceDataClass, multipleRequestSupported);
}
/**
- * send multiple commands to device
- *
- * @param method Method command belongs to
- * @param map {@code HashMap<String, Object> (name, value of parameter)}
+ * Send command to device with params and query info immediately
+ *
+ * @param command command
+ * @param deviceDataClass clazz contains devicedata which should be sent
+ * @param multipleRequestSupported set to true if device supports multipleRequests
*/
- public void sendDeviceCommands(String method, HashMap<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 = method;
- for (HashMap.Entry<String, Object> entry : map.entrySet()) {
- plBuilder.addParameter(entry.getKey(), entry.getValue());
- }
- String payload = plBuilder.getPayload();
-
- sendSecurePasstrhroug(payload, method);
+ public void sendCommandAndQuery(String command, Object deviceDataClass, boolean multipleRequestSupported) {
+ if (multipleRequestSupported) {
+ List<TapoRequest> requests = new ArrayList<>();
+ requests.add(new TapoRequest(command, deviceDataClass));
+ requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+ sendAsyncRequest(new TapoMultipleRequest(requests));
} else {
- logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
+ sendDeviceCommand(command, deviceDataClass, true);
+ queryAfterCommand = true;
}
}
/**
- * Query Info from Device and refresh deviceInfo
+ * Send command to child device
+ *
+ * @param childData ChildDeviceData-Class
*/
- public void queryInfo() {
- queryInfo(false);
+ public void sendChildCommand(TapoChildDeviceData childData) {
+ sendChildCommand(childData, false);
}
/**
- * Query Info from Device and refresh deviceInfo
- *
- *
+ * Send command to child device
+ *
+ * @param childData ChildDeviceData-Class
* @param ignoreGap ignore gap to last query. query anyway
*/
- public void queryInfo(boolean ignoreGap) {
- logger.trace("({}) DeviceConnector_queryInfo from '{}'", uid, deviceURL);
- queryCommand(DEVICE_CMD_GETINFO, ignoreGap);
+ public void sendChildCommand(TapoChildDeviceData childData, boolean ignoreGap) {
+ long now = System.currentTimeMillis();
+ if (ignoreGap || now > lastSent + TAPO_SEND_MIN_GAP_MS) {
+ lastSent = now;
+ sendAsyncRequest(new TapoChildRequest(childData));
+ } else {
+ logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastSent);
+ }
}
/**
- * Query Info from Child Devices and refresh deviceInfo
+ * send asynchronous multi-request
+ *
+ * @param requests list of TapoRequests should be sent to device
*/
- @Override
- public void queryChildDevices() {
- logger.trace("({}) DeviceConnector_queryChildDevices from '{}'", uid, deviceURL);
- queryCommand(DEVICE_CMD_CHILD_DEVICE_LIST, true);
+ public void sendMultipleRequest(List<TapoRequest> requests) {
+ sendMultipleRequest(requests, false);
}
/**
- * Get energy usage from device
+ * send asynchronous multi-request igrnoring min-gap
+ *
+ * @param requests list of TapoRequests should be sent to device
+ * @param ignoreGap ignoreGap ignore gap to last query. query anyway
*/
- public void getEnergyUsage() {
- queryCommand(DEVICE_CMD_GETENERGY, true);
+ public void sendMultipleRequest(List<TapoRequest> requests, boolean ignoreGap) {
+ queryAfterCommand = false;
+ long now = System.currentTimeMillis();
+ if (ignoreGap || now > lastQuery + TAPO_QUERY_MIN_GAP_MS) {
+ lastQuery = now;
+ sendAsyncRequest(new TapoMultipleRequest(requests));
+ } else {
+ logger.debug("({}) command not sent because of min_gap: {} <- {}", uid, now, lastSent);
+ }
}
/**
- * Send Custom DeviceQuery
- *
- * @param queryCommand Command to be queried
- * @param ignoreGap ignore gap to last query. query anyway
+ * send asynchron multi-request
+ *
+ * @param requests array of TapoRequest
*/
- public void queryCommand(String queryCommand, boolean ignoreGap) {
- logger.trace("({}) DeviceConnector_queryCommand '{}' from '{}'", uid, queryCommand, deviceURL);
- long now = System.currentTimeMillis();
- if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
- this.lastQuery = now;
-
- /* create payload */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = queryCommand;
- String payload = plBuilder.getPayload();
-
- sendSecurePasstrhroug(payload, queryCommand);
- } else {
- logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastQuery);
- }
+ public void sendMultipleRequest(TapoRequest... requests) {
+ sendMultipleRequest(requests);
}
/**
- * SEND SECUREPASSTHROUGH
- * encprypt payload and send to device
- *
- * @param payload payload sent to device
- * @param command command executed - this will handle result
+ * send asynchron request to protocol handler
+ *
+ * @param tapoRequest Request inherits TapoBaseRequestInterface
*/
- protected void sendSecurePasstrhroug(String payload, String command) {
- /* encrypt payload */
- logger.trace("({}) encrypting payload '{}'", uid, payload);
- String encryptedPayload = encryptPayload(payload);
+ public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) {
+ try {
+ protocolHandler.sendAsyncRequest(tapoRequest);
+ } catch (TapoErrorHandler tapoError) {
+ handleError(tapoError);
+ }
+ }
- /* create secured payload */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = "securePassthrough";
- plBuilder.addParameter("request", encryptedPayload);
- String securePassthroughPayload = plBuilder.getPayload();
+ /***********************
+ * Response-Handling
+ **********************/
- sendAsyncRequest(deviceURL, securePassthroughPayload, command);
+ /**
+ * Return class object from json formated string
+ *
+ * @param json json formatted string
+ * @param clazz class string should parsed to
+ */
+ private <T> T getObjectFromJson(String json, Class<T> clazz) {
+ try {
+ @Nullable
+ T result = GSON.fromJson(json, clazz);
+ if (result == null) {
+ throw new JsonParseException("result is null");
+ }
+ return result;
+ } catch (Exception e) {
+ logger.debug("({}) error parsing string {} to class: {}", uid, json, clazz.getName());
+ device.setError(new TapoErrorHandler(ERR_API_JSON_DECODE_FAIL));
+ return Objects.requireNonNull(GSON.fromJson(json, clazz));
+ }
}
- /***********************************
- *
- * HANDLE RESPONSES
- *
- ************************************/
+ /**
+ * Return class object from JsonObject
+ *
+ * @param jso JsonOject
+ * @param clazz class string should parsed to
+ */
+ private <T> T getObjectFromJson(JsonObject jso, Class<T> clazz) {
+ return getObjectFromJson(jso.toString(), clazz);
+ }
/**
- * Handle SuccessResponse (setDeviceInfo)
- *
- * @param responseBody String with responseBody from device
+ * handle and decrypt response from device
+ *
+ * @param response TapoResponse was received
+ * @param command was sent to device belonging to response
*/
@Override
- protected void handleSuccessResponse(String responseBody) {
- JsonObject jsnResult = getJsonFromResponse(responseBody);
- Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
- if (errorCode != 0) {
- logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
- this.device.handleConnectionState();
+ public void handleResponse(TapoResponse response, String command) {
+ if (!response.hasError()) {
+ if (DEVICE_CMD_MULTIPLE_REQ.equals(command)) {
+ handleMultipleRespone(response);
+ } else {
+ handleSingleResponse(response, command);
+ }
+ } else {
+ device.setError(new TapoErrorHandler(response.errorCode()));
}
- this.device.responsePasstrough(responseBody);
}
/**
- *
- * handle JsonResponse (getDeviceInfo)
- *
- * @param responseBody String with responseBody from device
+ * Handle response got from single-request
+ *
+ * @param response response from request
+ * @param command command was sent
*/
- @Override
- protected void handleDeviceResult(String responseBody) {
- JsonObject jsnResult = getJsonFromResponse(responseBody);
- if (jsnResult.has(JSON_KEY_ID)) {
- this.deviceInfo = new TapoDeviceInfo(jsnResult);
- this.device.setDeviceInfo(deviceInfo);
+ private void handleSingleResponse(TapoResponse response, String command) {
+ logger.trace("({}) handle singleResponse from command '{}'", uid, command);
+ if (DEVICE_CMDLIST_QUERY.contains(command)) {
+ handleQueryResult(response, command);
+ } else if (DEVICE_CMDLIST_SET.contains(command)) {
+ handleSuccessResponse(response);
} else {
- this.deviceInfo = new TapoDeviceInfo();
- this.device.handleConnectionState();
+ device.responsePasstrough(response);
}
- this.device.responsePasstrough(responseBody);
}
/**
- * handle JsonResponse (getEnergyData)
- *
- * @param responseBody String with responseBody from device
+ * Handle response got from multiple-request
+ *
+ * @param response response from request
*/
- @Override
- protected void handleEnergyResult(String responseBody) {
- JsonObject jsnResult = getJsonFromResponse(responseBody);
- if (jsnResult.has(JSON_KEY_ENERGY_POWER)) {
- this.energyData = new TapoEnergyData(jsnResult);
- this.device.setEnergyData(energyData);
- } else {
- this.energyData = new TapoEnergyData();
+ private void handleMultipleRespone(TapoResponse response) {
+ logger.trace("({}) handle multipleResponse '{}'", uid, response);
+ for (TapoResponse results : response.responses()) {
+ handleSingleResponse(results, results.method());
}
- this.device.responsePasstrough(responseBody);
}
/**
- * handle JsonResponse (getChildDeviceList)
- *
- * @param responseBody String with responseBody from device
+ * Parse responsedata from result to object and inform device about new data
+ *
+ * @param response response from request
+ * @param command command was sent
*/
- @Override
- protected void handleChildDevices(String responseBody) {
- JsonObject jsnResult = getJsonFromResponse(responseBody);
- if (jsnResult.has(JSON_KEY_CHILD_START_INDEX)) {
- this.childData = Objects.requireNonNull(GSON.fromJson(jsnResult, TapoChildData.class));
- this.device.setChildData(childData);
+ private void handleQueryResult(TapoResponse response, String command) {
+ if (!response.hasError()) {
+ logger.trace("({}) queryResponse successfull '{}'", uid, response);
+ queryResponse = response;
+ device.newDataResult(command);
} else {
- this.childData = new TapoChildData();
+ logger.debug("({}) query response returned error: {}", uid, response);
+ device.setError(new TapoErrorHandler(queryResponse.errorCode()));
}
- this.device.responsePasstrough(responseBody);
}
/**
- * handle custom response
- *
- * @param responseBody String with responseBody from device
+ * Handle SuccessResponse (setDeviceInfo)
+ *
+ * @param response response from request
*/
- @Override
- protected void handleCustomResponse(String responseBody) {
- this.device.responsePasstrough(responseBody);
+ private void handleSuccessResponse(TapoResponse response) {
+ if (response.hasError()) {
+ logger.debug("({}) set deviceInfo not successful: {}", uid, response);
+ device.setError(new TapoErrorHandler(response.errorCode()));
+ } else {
+ logger.trace("({}) setcommand successfull '{}'", uid, response);
+ if (queryAfterCommand) {
+ sendQueryCommand(DEVICE_CMD_GETINFO, true);
+ }
+ }
+ queryAfterCommand = false;
+ this.device.responsePasstrough(response);
}
/**
- * handle error
+ * Handle custom response
*
- * @param tapoError TapoErrorHandler
+ * @param response response from request
*/
- @Override
- protected void handleError(TapoErrorHandler tapoError) {
- this.device.setError(tapoError);
+ protected void handleCustomResponse(TapoResponse response) {
+ logger.trace("({}) handle custom response '{}'", uid, response);
+ this.device.responsePasstrough(response);
}
/**
- * get Json from response
- *
- * @param responseBody
- * @return JsonObject with result
+ * handle error
*/
- private JsonObject getJsonFromResponse(String responseBody) {
- JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
- /* get errocode (0=success) */
- if (jsonObject != null) {
- Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
- if (errorCode == 0) {
- /* decrypt response */
- jsonObject = GSON.fromJson(responseBody, JsonObject.class);
- logger.trace("({}) received result: {}", uid, responseBody);
- if (jsonObject != null) {
- /* return result if set / else request was successful */
- if (jsonObject.has("result")) {
- return jsonObject.getAsJsonObject("result");
- } else {
- return jsonObject;
- }
- }
- } else {
- /* return errorcode from device */
- TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
- logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
- handleError(te);
- return jsonObject;
- }
- }
- logger.debug("({}) sendPayload exception {}", uid, responseBody);
- handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
- return new JsonObject();
+ @Override
+ public void handleError(TapoErrorHandler tapoError) {
+ logger.debug("({}) handle error '{}'", uid, tapoError.getMessage());
+ device.setError(tapoError);
}
- /***********************************
- *
- * GET RESULTS
- *
- ************************************/
+ /***********************
+ * Get Values
+ **********************/
/**
* Check if device is online
*
* @return true if device is online
*/
- public Boolean isOnline() {
+ public boolean isOnline() {
return isOnline(false);
}
/**
* Check if device is online
*
- * @param raiseError if true
- * @return true if device is online
+ * @param raiseError if true - if false it will be only logout();
*/
- public Boolean isOnline(Boolean raiseError) {
+ public boolean isOnline(boolean raiseError) {
if (pingDevice()) {
return true;
} else {
if (raiseError) {
handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE));
}
- logout();
+ protocolHandler.logout();
return false;
}
}
/**
- * IP-Adress
- *
- * @return String ipAdress
+ * Get Dataobject from response
+ *
+ * @param clazz object class response should be transformed
*/
- public String getIP() {
- return this.ipAddress;
+ public <T> T getResponseData(Class<T> clazz) {
+ return getObjectFromJson(queryResponse.result(), clazz);
}
/**
- * PING IP Adress
- *
- * @return true if ping successfull
+ * Ping to IP of device - return true if successfull
*/
- public Boolean pingDevice() {
+ public boolean pingDevice() {
try {
- InetAddress address = InetAddress.getByName(this.ipAddress);
+ InetAddress address = InetAddress.getByName(device.getIpAddress());
return address.isReachable(TAPO_PING_TIMEOUT_MS);
} catch (Exception e) {
logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
}
}
- private Optional<TapoChild> getChild(int position) {
- return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
+ @Override
+ public HttpClient getHttpClient() {
+ return bridge.getHttpClient();
+ }
+
+ @Override
+ public void responsePasstrough(String response, String command) {
+ throw new UnsupportedOperationException("Unimplemented method 'responsePasstrough'");
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return device.getIpAddress() + ":" + device.getDeviceConfig().httpPort;
+ }
+
+ @Override
+ public String getThingUID() {
+ return device.getThingUID().toString();
}
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.api;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.eclipse.jetty.client.HttpResponse;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.client.util.BufferingResponseListener;
-import org.eclipse.jetty.client.util.StringContentProvider;
-import org.eclipse.jetty.http.HttpMethod;
-import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
-import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
-import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoCipher;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.FieldNamingPolicy;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home device connections.
- * This class uses synchronous HttpClient-Requests for login to device
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoDeviceHttpApi {
- protected static final Gson GSON = new GsonBuilder()
- .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
-
- private final Logger logger = LoggerFactory.getLogger(TapoDeviceHttpApi.class);
- private final TapoCipher tapoCipher;
- private final TapoBridgeHandler bridge;
- protected final String uid;
- protected final TapoDevice device;
-
- private String token = "";
- private String cookie = "";
- protected String deviceURL = "";
- protected String ipAddress = "";
-
- /**
- * INIT CLASS
- *
- * @param device
- * @param bridgeThingHandler
- */
- public TapoDeviceHttpApi(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
- this.bridge = bridgeThingHandler;
- this.tapoCipher = new TapoCipher();
- this.device = device;
- this.uid = device.getThingUID().getAsString();
- setDeviceURL(device.getIpAddress());
- }
-
- /***********************************
- *
- * DELEGATING FUNCTIONS
- * will normaly be delegated to extension-classes(TapoDeviceConnector)
- *
- ************************************/
- /**
- * handle SuccessResponse (setDeviceInfo)
- *
- * @param responseBody String with responseBody from device
- */
- protected void handleSuccessResponse(String responseBody) {
- }
-
- /**
- * handle JsonResponse (getDeviceInfo)
- *
- * @param responseBody String with responseBody from device
- */
- protected void handleDeviceResult(String responseBody) {
- }
-
- /**
- * handle JsonResponse (getEnergyData)
- *
- * @param responseBody String with responseBody from device
- */
- protected void handleEnergyResult(String responseBody) {
- }
-
- /**
- * handle custom response
- *
- * @param responseBody String with responseBody from device
- */
- protected void handleCustomResponse(String responseBody) {
- }
-
- /**
- * handle JsonResponse (getChildDevices)
- *
- * @param responseBody String with responseBody from device
- */
- protected void handleChildDevices(String responseBody) {
- }
-
- /**
- * handle error
- *
- * @param tapoError TapoErrorHandler
- */
- protected void handleError(TapoErrorHandler tapoError) {
- }
-
- /**
- * refresh the list of child devices
- *
- */
- protected void queryChildDevices() {
- }
-
- /***********************************
- *
- * LOGIN FUNCTIONS
- *
- ************************************/
-
- /**
- * Create Handshake and set cookie
- *
- * @return true if handshake (cookie) was created
- */
- protected String createHandshake() {
- String cookie = "";
- try {
- /* create payload for handshake */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = "handshake";
- plBuilder.addParameter("key", bridge.getCredentials().getPublicKey()); // ?.decode("UTF-8")
- String payload = plBuilder.getPayload();
-
- /* send request (create ) */
- logger.trace("({}) create handhsake with payload: {}", uid, payload);
- ContentResponse response = sendRequest(this.deviceURL, payload);
- if (response != null && getErrorCode(response) == 0) {
- String encryptedKey = getKeyFromResponse(response);
- this.tapoCipher.setKey(encryptedKey, bridge.getCredentials());
- cookie = getCookieFromResponse(response);
- }
- } catch (Exception e) {
- logger.debug("({}) could not createHandshake: {}", uid, e.toString());
- handleError(new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, "could not createHandshake"));
- }
- return cookie;
- }
-
- /**
- * return encrypted key from 'handshake' request
- *
- * @param response ContentResponse from "handshake" method
- * @return
- */
- private String getKeyFromResponse(ContentResponse response) {
- String rBody = response.getContentAsString();
- JsonObject jsonObj = GSON.fromJson(rBody, JsonObject.class);
- if (jsonObj != null) {
- logger.trace("({}) received awnser: {}", uid, rBody);
- return jsonObjectToString(jsonObj.getAsJsonObject("result"), "key");
- } else {
- logger.warn("({}) could not getKeyFromResponse '{}'", uid, rBody);
- handleError(new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, "could not getKeyFromResponse"));
- }
- return "";
- }
-
- /**
- * return cookie from 'handshake' request
- *
- * @param response ContentResponse from "handshake" metho
- * @return
- */
- private String getCookieFromResponse(ContentResponse response) {
- String cookie = "";
- try {
- cookie = response.getHeaders().get("Set-Cookie").split(";")[0];
- logger.trace("({}) got cookie: '{}'", uid, cookie);
- } catch (Exception e) {
- logger.warn("({}) could not getCookieFromResponse", uid);
- handleError(new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, "could not getCookieFromResponse"));
- }
- return cookie;
- }
-
- /**
- * Query Token from device
- *
- * @return String with token returned from device
- */
- protected String queryToken() {
- String token = "";
- try {
- /* encrypt login credentials */
- PayloadBuilder plBuilder = new PayloadBuilder();
- plBuilder.method = "login_device";
- plBuilder.addParameter("username", bridge.getCredentials().getEncodedEmail());
- plBuilder.addParameter("password", bridge.getCredentials().getEncodedPassword());
- String payload = plBuilder.getPayload();
- String encryptedPayload = this.encryptPayload(payload);
-
- /* create secured login informations */
- plBuilder = new PayloadBuilder();
- plBuilder.method = "securePassthrough";
- plBuilder.addParameter("request", encryptedPayload);
- String securePassthroughPayload = plBuilder.getPayload();
-
- /* sendRequest and get Token */
- ContentResponse response = sendRequest(deviceURL, securePassthroughPayload);
- token = getTokenFromResponse(response);
- } catch (Exception e) {
- logger.debug("({}) error building login payload: {}", uid, e.toString());
- handleError(new TapoErrorHandler(e, "error building login payload"));
- }
- return token;
- }
-
- /**
- * get Token from "login"-request
- *
- * @param response
- * @return
- */
- private String getTokenFromResponse(@Nullable ContentResponse response) {
- String result = "";
- TapoErrorHandler tapoError = new TapoErrorHandler();
- if (response != null && response.getStatus() == 200) {
- String rBody = response.getContentAsString();
- String decryptedResponse = this.decryptResponse(rBody);
- logger.trace("({}) received result: {}", uid, decryptedResponse);
-
- /* get errocode (0=success) */
- JsonObject jsonObject = GSON.fromJson(decryptedResponse, JsonObject.class);
- if (jsonObject != null) {
- Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
- if (errorCode == 0) {
- /* return result if set / else request was successful */
- result = jsonObjectToString(jsonObject.getAsJsonObject("result"), "token");
- } else {
- /* return errorcode from device */
- tapoError.raiseError(errorCode, "could not get token");
- logger.debug("({}) login recieved errorCode {} - {}", uid, errorCode, tapoError.getMessage());
- }
- } else {
- logger.debug("({}) unexpected json-response '{}'", uid, decryptedResponse);
- tapoError.raiseError(ERR_API_JSON_ENCODE_FAIL, "could not get token");
- }
- } else {
- logger.debug("({}) invalid response while login", uid);
- tapoError.raiseError(ERR_BINDING_HTTP_RESPONSE, "invalid response while login");
- }
- /* handle error */
- if (tapoError.hasError()) {
- handleError(tapoError);
- }
- return result;
- }
-
- /***********************************
- *
- * HTTP-ACTIONS
- *
- ************************************/
- /**
- * SEND SYNCHRON HTTP-REQUEST
- *
- * @param url url request is sent to
- * @param payload payload (String) to send
- * @return ContentResponse of request
- */
- @Nullable
- protected ContentResponse sendRequest(String url, String payload) {
- logger.trace("({}) sendRequest to '{}' with cookie '{}'", uid, url, this.cookie);
-
- Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
-
- /* set header */
- httpRequest = setHeaders(httpRequest);
- httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
-
- /* add request body */
- httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
-
- try {
- return httpRequest.send();
- } catch (InterruptedException e) {
- logger.debug("({}) sending request interrupted: {}", uid, e.toString());
- handleError(new TapoErrorHandler(e));
- } catch (TimeoutException e) {
- logger.debug("({}) sending request timeout: {}", uid, e.toString());
- handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, e.toString()));
- } catch (Exception e) {
- logger.debug("({}) sending request failed: {}", uid, e.toString());
- handleError(new TapoErrorHandler(e));
- }
- return null;
- }
-
- /**
- * SEND ASYNCHRONOUS HTTP-REQUEST
- * (don't wait for awnser with programm code)
- *
- * @param url string url request is sent to
- * @param payload data-payload
- * @param command command executed - this will handle RepsonseType
- */
- protected void sendAsyncRequest(String url, String payload, String command) {
- logger.trace("({}) sendAsncRequest to '{}' with cookie '{}'", uid, url, this.cookie);
- logger.trace("({}) command/payload: '{}''{}'", uid, command, payload);
-
- try {
- Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
-
- /* set header */
- httpRequest = setHeaders(httpRequest);
-
- /* add request body */
- httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
-
- httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
- @NonNullByDefault({})
- @Override
- public void onComplete(Result result) {
- final HttpResponse response = (HttpResponse) result.getResponse();
- if (result.getFailure() != null) {
- /* handle result errors */
- Throwable e = result.getFailure();
- String errorMessage = getValueOrDefault(e.getMessage(), "");
- if (e instanceof TimeoutException) {
- logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
- handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
- } else {
- logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
- handleError(new TapoErrorHandler(new Exception(e), errorMessage));
- }
- } else if (response.getStatus() != 200) {
- logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
- handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
- } else {
- /* request successful */
- String rBody = getContentAsString();
- logger.trace("({}) receivedRespose '{}'", uid, rBody);
- if (!hasErrorCode(rBody)) {
- rBody = decryptResponse(rBody);
- logger.trace("({}) decryptedResponse '{}'", uid, rBody);
- /* handle result */
- switch (command) {
- case DEVICE_CMD_SETINFO:
- handleSuccessResponse(rBody);
- break;
- case DEVICE_CMD_GETINFO:
- handleDeviceResult(rBody);
- break;
- case DEVICE_CMD_GETENERGY:
- handleEnergyResult(rBody);
- break;
- case DEVICE_CMD_CUSTOM:
- handleCustomResponse(rBody);
- break;
- case DEVICE_CMD_CHILD_DEVICE_LIST:
- handleChildDevices(rBody);
- break;
- }
- } else {
- getErrorCode(rBody);
- }
- }
- }
- });
- } catch (Exception e) {
- handleError(new TapoErrorHandler(e));
- }
- }
-
- /**
- * return error code from response
- *
- * @param response
- * @return 0 if request was successfull
- */
- protected Integer getErrorCode(@Nullable ContentResponse response) {
- try {
- if (response != null) {
- String responseBody = response.getContentAsString();
- return getErrorCode(responseBody);
- } else {
- return ERR_BINDING_HTTP_RESPONSE.getCode();
- }
- } catch (Exception e) {
- return ERR_BINDING_HTTP_RESPONSE.getCode();
- }
- }
-
- /**
- * return error code from responseBody
- *
- * @param responseBody
- * @return 0 if request was successfull
- */
- protected Integer getErrorCode(String responseBody) {
- try {
- JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
- /* get errocode (0=success) */
- Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
- if (errorCode == 0) {
- return 0;
- } else {
- logger.debug("({}) device returns errorcode '{}'", uid, errorCode);
- handleError(new TapoErrorHandler(errorCode));
- return errorCode;
- }
- } catch (Exception e) {
- return ERR_BINDING_HTTP_RESPONSE.getCode();
- }
- }
-
- /**
- * Check for JsonObject "errorcode" and if this is > 0 (no Error)
- *
- * @param responseBody
- * @return true if is js errorcode > 0; false if there is no "errorcode"
- */
- protected Boolean hasErrorCode(String responseBody) {
- if (isValidJson(responseBody)) {
- JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
- /* get errocode (0=success) */
- Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
- if (errorCode > 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * SET HTTP-HEADERS
- */
- private Request setHeaders(Request httpRequest) {
- /* set header */
- httpRequest.header("content-type", CONTENT_TYPE_JSON);
- httpRequest.header("Accept", CONTENT_TYPE_JSON);
- if (!this.cookie.isEmpty()) {
- httpRequest.header(HTTP_AUTH_TYPE_COOKIE, this.cookie);
- }
- return httpRequest;
- }
-
- /***********************************
- *
- * ENCRYPTION / CODING
- *
- ************************************/
-
- /**
- * Decrypt Response
- *
- * @param responseBody encrypted string from response-body
- * @return String decrypted responseBody
- */
- protected String decryptResponse(String responseBody) {
- try {
- JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
- if (jsonObject != null) {
- String encryptedResponse = jsonObjectToString(jsonObject.getAsJsonObject("result"), "response");
- return tapoCipher.decode(encryptedResponse);
- } else {
- handleError(new TapoErrorHandler(ERR_API_JSON_DECODE_FAIL));
- }
- } catch (Exception ex) {
- logger.debug("({}) exception '{}' decryptingResponse: '{}'", uid, ex.toString(), responseBody);
- }
- return responseBody;
- }
-
- /**
- * encrypt payload
- *
- * @param payload
- * @return encrypted payload
- */
- protected String encryptPayload(String payload) {
- try {
- return tapoCipher.encode(payload);
- } catch (Exception ex) {
- logger.debug("({}) exception encoding Payload '{}'", uid, ex.toString());
- return "";
- }
- }
-
- /**
- * perform logout (dispose cookie)
- */
- public void logout() {
- logger.trace("DeviceHttpApi_logout");
- unsetToken();
- unsetCookie();
- }
-
- /***********************************
- *
- * GET RESULTS
- *
- ************************************/
- /**
- * Logged In
- *
- * @return true if logged in
- */
- public Boolean loggedIn() {
- return loggedIn(false);
- }
-
- /**
- * Logged In
- *
- * @param raiseError if true
- * @return true if logged in
- */
- public Boolean loggedIn(Boolean raiseError) {
- if (!this.token.isBlank() && !this.cookie.isBlank()) {
- return true;
- } else {
- logger.trace("({}) not logged in", uid);
- if (raiseError) {
- handleError(new TapoErrorHandler(ERR_API_LOGIN));
- }
- return false;
- }
- }
-
- /***********************************
- *
- * SET VALUES
- *
- ************************************/
-
- /**
- * Set new ipAddress
- *
- * @param ipAddress new ipAdress
- */
- public void setDeviceURL(String ipAddress) {
- this.ipAddress = ipAddress;
- this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
- }
-
- /**
- * Set new ipAdresss with token
- *
- * @param ipAddress ipAddres of device
- * @param token token from login-ressult
- */
- public void setDeviceURL(String ipAddress, String token) {
- this.ipAddress = ipAddress;
- this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
- }
-
- /**
- * Set new token
- *
- * @param token
- */
- protected void setToken(String token) {
- if (!token.isBlank()) {
- String url = this.deviceURL.replaceAll("\\?token=\\w*", "");
- this.deviceURL = url + "?token=" + token;
- }
- this.token = token;
- }
-
- /**
- * Unset Token (device logout)
- */
- protected void unsetToken() {
- this.deviceURL = this.deviceURL.replaceAll("\\?token=\\w*", "");
- this.token = "";
- }
-
- /**
- * Set new cookie
- *
- * @param cookie
- */
- protected void setCookie(String cookie) {
- this.cookie = cookie;
- }
-
- /**
- * Unset Cookie (device logout)
- */
- protected void unsetCookie() {
- bridge.getHttpClient().getCookieStore().removeAll();
- this.cookie = "";
- }
-}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol;
+
+import java.util.EnumSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Enumeration for Tapo-Protocols
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public enum TapoProtocolEnum {
+ PASSTHROUGH(""),
+ SECUREPASSTROUGH("AES"),
+ KLAP("KLAP");
+
+ public final String value;
+
+ private TapoProtocolEnum(String value) {
+ this.value = value;
+ }
+
+ public static TapoProtocolEnum valueOfString(String label) {
+ return EnumSet.allOf(TapoProtocolEnum.class).stream().filter(p -> p.value.equals(label)).findFirst()
+ .orElseThrow(() -> new IllegalArgumentException());
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return getValue();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * Interface for TAPO-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public interface TapoProtocolInterface {
+
+ /***********************
+ * Login Handling
+ **********************/
+ public boolean login(TapoCredentials credentials) throws TapoErrorHandler;
+
+ public void logout();
+
+ public boolean isLoggedIn();
+
+ /***********************
+ * Request Sender
+ **********************/
+ /* send synchron request - response will be handled in [responseReceived()] function */
+ public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler;
+
+ /* send asynchron request - response will be handled in [asyncResponseReceived()] function */
+ public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler;
+
+ /************************
+ * RESPONSE HANDLERS
+ ************************/
+
+ /* handle synchron request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function */
+ public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler;
+
+ /* handle asynchron request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function */
+ public void asyncResponseReceived(String content, String command) throws TapoErrorHandler;
+
+ /************************
+ * PRIVATE HELPERS
+ ************************/
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.aes;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO-SECUREPASSTHROUGH-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class SecurePassthrough implements TapoProtocolInterface {
+ private final Logger logger = LoggerFactory.getLogger(SecurePassthrough.class);
+ protected final TapoConnectorInterface httpDelegator;
+ private final SecurePassthroughSession session;
+ private final String uid;
+
+ /***********************
+ * Init Class
+ **********************/
+
+ public SecurePassthrough(TapoConnectorInterface httpDelegator) {
+ this.httpDelegator = httpDelegator;
+ session = new SecurePassthroughSession(this);
+ uid = httpDelegator.getThingUID() + " / HTTP-SecurePasstrhough";
+ }
+
+ /***********************
+ * Login Handling
+ **********************/
+
+ @Override
+ public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
+ logger.trace("({}) login to device", uid);
+ session.reset();
+ session.login(tapoCredentials);
+ return session.isHandshakeComplete();
+ }
+
+ @Override
+ public void logout() {
+ logger.trace("({}) logout from device", uid);
+ session.reset();
+ }
+
+ @Override
+ public boolean isLoggedIn() {
+ return session.isHandshakeComplete();
+ }
+
+ /***********************
+ * Request Sender
+ **********************/
+
+ /*
+ * send synchron request - request will be sent encrypted (secured)
+ * response will be handled in [responseReceived()] function
+ */
+ @Override
+ public void sendRequest(TapoRequest request) throws TapoErrorHandler {
+ sendRequest(request, true);
+ }
+
+ /*
+ * send synchron request - response will be handled in [responseReceived()] funktion
+ *
+ * @param encrypt - if false response will be sent unsecured
+ */
+ private void sendRequest(TapoRequest tapoRequest, boolean encrypt) throws TapoErrorHandler {
+ String url = getUrl();
+ String command = tapoRequest.method();
+ logger.trace("({}) sending unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+ if (encrypt) {
+ tapoRequest = session.encryptRequest(tapoRequest);
+ logger.trace("({}) encrypted request: '{}' with cookie '{}'", uid, tapoRequest, session.getCookie());
+ }
+
+ Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ /* set header */
+ httpRequest = setHeaders(httpRequest);
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ /* add request body */
+ httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+ try {
+ responseReceived(httpRequest.send(), command);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(e, "error sending request");
+ }
+ }
+
+ /*
+ * send asynchron request - request will be sent encrypted (secured)
+ * response will be handled in [asyncResponseReceived()] function
+ */
+ @Override
+ public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
+ String url = getUrl();
+ String command = tapoRequest.method();
+ logger.trace("({}) sendAsync unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+
+ TapoRequest encryptedRequest = session.encryptRequest(tapoRequest);
+ logger.trace("({}) sending encrypted request to '{}' with cookie '{}'", uid, url, session.getCookie());
+
+ Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ /* set header */
+ httpRequest = setHeaders(httpRequest);
+
+ /* add request body */
+ httpRequest.content(new StringContentProvider(encryptedRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
+ @NonNullByDefault({})
+ @Override
+ public void onComplete(Result result) {
+ final HttpResponse response = (HttpResponse) result.getResponse();
+ if (result.getFailure() != null) {
+ /* handle result errors */
+ Throwable e = result.getFailure();
+ String errorMessage = getValueOrDefault(e.getMessage(), "");
+ /* throw errors to delegator */
+ if (e instanceof TimeoutException) {
+ logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
+ } else {
+ logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
+ httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
+ }
+ } else if (response.getStatus() != 200) {
+ logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
+ } else {
+ /* request successful */
+ String rBody = getContentAsString();
+ try {
+ asyncResponseReceived(rBody, command);
+ } catch (TapoErrorHandler tapoError) {
+ httpDelegator.handleError(tapoError);
+ }
+ }
+ }
+ });
+ }
+
+ /************************
+ * RESPONSE HANDLERS
+ ************************/
+
+ /**
+ * handle synchron request-response
+ * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
+ logger.trace("({}) recived response: {}", uid, response.getContentAsString());
+ TapoResponse tapoResponse = getTapoResponse(response);
+ httpDelegator.handleResponse(tapoResponse, command);
+ httpDelegator.responsePasstrough(response.getContentAsString(), command);
+ }
+
+ /**
+ * handle asynchron request-response
+ * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
+ logger.trace("({}) asyncResponseReceived '{}'", uid, content);
+ try {
+ TapoResponse tapoResponse = getTapoResponse(content);
+ httpDelegator.handleResponse(tapoResponse, command);
+ } catch (TapoErrorHandler tapoError) {
+ httpDelegator.handleError(tapoError);
+ }
+ }
+
+ /**
+ * Get Tapo-Response from Contentresponse
+ * decrypt if is encrypted
+ */
+ protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
+ if (response.getStatus() == 200) {
+ return getTapoResponse(response.getContentAsString());
+ } else {
+ logger.debug("({}) invalid response received", uid);
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /**
+ * Get Tapo-Response from responsestring
+ * decrypt if is encrypted
+ */
+ protected TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
+ if (isValidJson(responseString)) {
+ TapoResponse tapoResponse = Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
+ if (tapoResponse.result().has("response")) {
+ tapoResponse = session.decryptResponse(tapoResponse);
+ }
+ if (tapoResponse.hasError()) {
+ throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+ }
+ return tapoResponse;
+ } else {
+ logger.debug("({}) invalid response received", uid);
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /************************
+ * PRIVATE HELPERS
+ ************************/
+
+ /**
+ * Get Url requests are sent to
+ */
+ protected String getUrl() {
+ String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
+ if (session.isHandshakeComplete()) {
+ return baseUrl + "?token=" + session.getToken();
+ } else {
+ return baseUrl;
+ }
+ }
+
+ /**
+ * Set HTTP-Headers
+ */
+ protected Request setHeaders(Request httpRequest) {
+ httpRequest.header("content-type", CONTENT_TYPE_JSON);
+ httpRequest.header("Accept", CONTENT_TYPE_JSON);
+ if (session.isHandshakeComplete()) {
+ httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());
+ }
+ return httpRequest;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.aes;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoKeyPair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO-SECUREPASSTHROUGH-SESSION
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class SecurePassthroughSession {
+ private static final int RSA_KEYSIZE = 1024;
+
+ private final Logger logger = LoggerFactory.getLogger(SecurePassthroughSession.class);
+
+ private final SecurePasstroughCipher cipher = new SecurePasstroughCipher();
+ private final TapoKeyPair tapoKeyPair;
+ private final SecurePassthrough spth;
+ private String cookie = "";
+ private String token = "";
+ private String uid;
+
+ // List of class-specific commands
+ public static final String DEVICE_CMD_GET_TOKEN = "login_device";
+ public static final String DEVICE_CMD_GET_COOKIE = "handshake";
+ public static final String DEVICE_CMD_SECURE_METHOD = "securePassthrough";
+
+ public SecurePassthroughSession(SecurePassthrough sptHandler) {
+ tapoKeyPair = new TapoKeyPair(RSA_KEYSIZE);
+ spth = sptHandler;
+ uid = spth.httpDelegator.getThingUID() + " / SecurePassthrough-Session";
+ }
+
+ public void reset() {
+ unsetCookie();
+ unsetToken();
+ }
+
+ /***********************
+ * Request Sender
+ **********************/
+
+ private ContentResponse sendHandshakeRequest(TapoRequest tapoRequest, boolean encrypt)
+ throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+ String url = spth.getUrl();
+ logger.trace("({}) sending Handshake request: '{}' to '{}' ", uid, tapoRequest, url);
+ Request httpRequest = spth.httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ if (encrypt) {
+ tapoRequest = encryptRequest(tapoRequest);
+ logger.trace("({}) encrypted request: '{}' with cookie '{}'", uid, tapoRequest, cookie);
+ }
+
+ /* set header */
+ httpRequest = spth.setHeaders(httpRequest);
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ /* add request body */
+ httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+ return httpRequest.send();
+ }
+
+ /************************
+ * HANDSHAKE & COOKIE
+ ************************/
+ /**
+ * Create Handshake
+ */
+ public boolean login(TapoCredentials credentials) throws TapoErrorHandler {
+ reset();
+ try {
+ TapoCredentials encodedCredentials = encodeCredentials(credentials);
+ createHandshake();
+ createToken(encodedCredentials);
+ } catch (TapoErrorHandler e) {
+ throw e;
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_API_LOGIN_FAILED);
+ }
+ return isHandshakeComplete();
+ }
+
+ /************************
+ * HANDSHAKE & COOKIE
+ ************************/
+
+ /**
+ * Create Handshake and get cookie
+ */
+ private void createHandshake() throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+ /* create handshake request */
+ JsonObject json = new JsonObject();
+ json.addProperty("key", tapoKeyPair.getPublicKey());
+ TapoRequest handshakeRequest = new TapoRequest(DEVICE_CMD_GET_COOKIE, json);
+ /* send request */
+ logger.trace("({}) create handhsake with payload: {}", uid, handshakeRequest);
+ ContentResponse response = sendHandshakeRequest(handshakeRequest, false);
+ handleHandshakeResponse(response);
+ }
+
+ /**
+ * work with response from handshake request
+ * get cookie from request and set cipher
+ */
+ private void handleHandshakeResponse(ContentResponse response) throws TapoErrorHandler {
+ /* setCookie */
+ String result = response.getHeaders().get("Set-Cookie").split(";")[0];
+ setCookie(result);
+
+ TapoResponse tapoResponse = spth.getTapoResponse(response);
+ if (!tapoResponse.hasError()) {
+ String encryptedKey = tapoResponse.result().get("key").getAsString();
+ cipher.setKey(encryptedKey, tapoKeyPair);
+ } else {
+ logger.debug("({}) could not createHandshake: {} ({})", uid, tapoResponse.message(),
+ tapoResponse.errorCode());
+ throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+ }
+ }
+
+ /**
+ * query Token from device with encoded credentials
+ */
+ private void createToken(TapoCredentials encodedCredentials)
+ throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+ /* create handshake request */
+ JsonObject json = new JsonObject();
+ json.addProperty("username", encodedCredentials.username());
+ json.addProperty("password", encodedCredentials.password());
+ TapoRequest loginRequest = new TapoRequest(DEVICE_CMD_GET_TOKEN, json);
+
+ /* send request */
+ ContentResponse response = sendHandshakeRequest(loginRequest, true);
+ handleTokenResponse(response);
+ }
+
+ /**
+ * get token from "login"-request
+ */
+ private void handleTokenResponse(ContentResponse response) throws TapoErrorHandler {
+ TapoResponse tapoResponse = spth.getTapoResponse(response);
+ if (!tapoResponse.hasError()) {
+ setToken(jsonObjectToString(tapoResponse.result(), "token"));
+ } else {
+ logger.debug("({}) invalid response while login: {} ({})", uid, tapoResponse.message(),
+ tapoResponse.errorCode());
+ throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+ }
+ }
+
+ private void setToken(String token) {
+ this.token = token;
+ }
+
+ private void unsetToken() {
+ token = "";
+ }
+
+ /**
+ * Cookie Handling
+ */
+ private void setCookie(String cookie) {
+ this.cookie = cookie;
+ }
+
+ private void unsetCookie() {
+ spth.httpDelegator.getHttpClient().getCookieStore().removeAll();
+ this.cookie = "";
+ }
+
+ /************************
+ * GET VALUES
+ ************************/
+
+ public String getCookie() {
+ return cookie;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public SecurePasstroughCipher getCipher() {
+ return cipher;
+ }
+
+ public boolean isHandshakeComplete() {
+ return !cookie.isBlank() && !token.isBlank();
+ }
+
+ /***********************************
+ * ENCRYPTION / CODING
+ ************************************/
+
+ private TapoCredentials encodeCredentials(TapoCredentials credentials) throws NoSuchAlgorithmException {
+ String username = TapoEncoder.b64Encode(TapoEncoder.sha1Encode(credentials.username()));
+ String password = TapoEncoder.b64Encode(credentials.password());
+ return new TapoCredentials(username, password);
+ }
+
+ /**
+ * Decrypt encrypted TapoResponse
+ */
+ public TapoResponse decryptResponse(TapoResponse response) throws TapoErrorHandler {
+ if (response.result().has("response")) {
+ try {
+ String encryptedResponse = response.result().get("response").getAsString();
+ String decryptedResponse = cipher.decode(encryptedResponse);
+ logger.trace("({}) decrypted response '{}'", uid, decryptedResponse);
+ return Objects.requireNonNull(GSON.fromJson(decryptedResponse, TapoResponse.class));
+ } catch (Exception e) {
+ logger.debug("({}) exception '{}' decryptingResponse: '{}'", uid, e, response);
+ throw new TapoErrorHandler(ERR_DATA_DECRYPTING);
+ }
+ }
+ throw new TapoErrorHandler(ERR_DATA_FORMAT);
+ }
+
+ /**
+ * Encrypt Request
+ */
+ public TapoRequest encryptRequest(Object request) throws TapoErrorHandler {
+ try {
+ JsonObject jso = new JsonObject();
+ jso.addProperty("request", cipher.encode(GSON.toJson(request)));
+ return new TapoRequest(DEVICE_CMD_SECURE_METHOD, jso);
+ } catch (Exception e) {
+ logger.debug("({}) exception encoding Payload '{}'", uid, e.toString());
+ throw new TapoErrorHandler(ERR_DATA_ENCRYPTING);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.aes;
+
+import static java.util.Base64.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+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.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoKeyPair;
+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 SecurePasstroughCipher {
+ private final Logger logger = LoggerFactory.getLogger(SecurePasstroughCipher.class);
+ protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+ protected static final String CIPHER_ALGORITHM = "AES";
+ protected static final String CIPHER_CHARSET = "UTF-8";
+ 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;
+
+ /**
+ * CREATE NEW EMPTY CIPHER
+ */
+ public SecurePasstroughCipher() {
+ }
+
+ /**
+ * CREATE NEW CIPHER WITH KEY AND CREDENTIALS
+ *
+ * @param handshakeKey key from Handshake-Request
+ * @param keyPair keyPair
+ * @throws TapoErrorHandler
+ */
+ public SecurePasstroughCipher(String handshakeKey, TapoKeyPair keyPair) throws TapoErrorHandler {
+ setKey(handshakeKey, keyPair);
+ }
+
+ /**
+ * SET NEW KEY AND CREDENTIALS
+ *
+ * @param handshakeKey key from Handshake-Request
+ * @param keyPair keyPair
+ */
+ public void setKey(String handshakeKey, TapoKeyPair keyPair) throws TapoErrorHandler {
+ logger.trace("Init passtroughCipher with key: {} ", handshakeKey);
+ try {
+ byte[] decode = getMimeDecoder().decode(handshakeKey.getBytes(HANDSHAKE_CHARSET));
+ byte[] decode2 = getMimeDecoder().decode(keyPair.getPrivateKeyBytes());
+ Cipher instance = Cipher.getInstance(HANDSHAKE_TRANSFORMATION);
+ KeyFactory kf = KeyFactory.getInstance(HANDSHAKE_ALGORITHM);
+ PrivateKey p = kf.generatePrivate(new PKCS8EncodedKeySpec(decode2));
+ 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 e) {
+ logger.warn("handshake Failed: {}", e.getMessage());
+ throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, e.getMessage());
+ }
+ }
+
+ /**
+ * INIT ENCODE/DECDE-CIPHERS
+ *
+ * @param bArr
+ * @param bArr2
+ * @throws Exception
+ */
+ protected void initCipher(byte[] bArr, byte[] bArr2) throws Exception {
+ try {
+ SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, CIPHER_ALGORITHM);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
+ encodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ decodeCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ encodeCipher.init(1, secretKeySpec, ivParameterSpec);
+ decodeCipher.init(2, secretKeySpec, ivParameterSpec);
+ } catch (Exception e) {
+ logger.warn("initChiper failed: {}", e.getMessage());
+ encodeCipher = null;
+ 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 = encodeCipher.doFinal(str.getBytes(CIPHER_CHARSET));
+ String encrypted = getMimeEncoder().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 = getMimeDecoder().decode(str.getBytes(CIPHER_CHARSET));
+ byte[] doFinal;
+ doFinal = decodeCipher.doFinal(data);
+ return new String(doFinal);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.klap;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * Cipher for KLAP-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class KlapCipher {
+ protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
+ protected static final String CIPHER_ALGORITHM = "AES";
+ protected static final String CIPHER_CHARSET = "UTF-8";
+
+ private int ivSeq = 0;
+ private byte[] iv;
+ private byte[] key;
+ private byte[] sig;
+
+ /************************
+ * INIT CLASS
+ ************************/
+
+ /**
+ * Init KlapCipher with seeds generated by session and got from device
+ *
+ * @param localSeed local random 16 byte seedhash sent to device
+ * @param remoteSeed 16 byte seed received as awnser from device
+ * @param userHash sha256 hash of user-credentials
+ */
+ public KlapCipher(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+ throws TapoErrorHandler, NoSuchAlgorithmException {
+ iv = initIvDerive(localSeed, remoteSeed, userHash);
+ key = initKeyDerive(localSeed, remoteSeed, userHash);
+ sig = initSigDerive(localSeed, remoteSeed, userHash);
+ }
+
+ /**
+ * reset all data (logout)
+ */
+ public void reset() {
+ ivSeq = 0;
+ iv = new byte[0];
+ key = new byte[0];
+ sig = new byte[0];
+ }
+
+ /************************
+ * GET VALUES
+ ************************/
+
+ /**
+ * return true if cipher is fully initialized (handshakes and cookies completed)
+ */
+ public boolean isInitialized() {
+ return iv.length > 0 && key.length > 0 && sig.length > 0;
+ }
+
+ /**
+ * get iv-sequence number which increments every request
+ */
+ public int getIvSeq() {
+ return ivSeq;
+ }
+
+ /************************
+ * ENCODING / DECODING
+ ************************/
+
+ /**
+ * ENCRYPT STRING TO BYTEARRAY
+ */
+ public byte[] encrypt(String str) throws TapoErrorHandler {
+ try {
+ ivSeq += 1; /* increment sequence */
+
+ Cipher encodeCipher = getCipher(Cipher.ENCRYPT_MODE, ivSeq);
+ byte[] msg = str.getBytes(CIPHER_CHARSET);
+ byte[] cipherText = encodeCipher.doFinal(msg);
+ byte[] signature = sha256Encode(concatBytes(sig, BigInteger.valueOf(ivSeq).toByteArray(), cipherText));
+ return concatBytes(signature, cipherText);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_DATA_ENCRYPTING, e.getMessage());
+ }
+ }
+
+ /**
+ * DECRYPT BYTEARRAY INTO STRING
+ */
+ public String decrypt(byte[] byteArr, int ivSeq) throws TapoErrorHandler {
+ try {
+ Cipher decodeCipher = getCipher(Cipher.DECRYPT_MODE, ivSeq);
+ byte[] bytesToDecode = truncateByteArray(byteArr, 32, byteArr.length - 32);
+ byte[] doFinal = decodeCipher.doFinal(bytesToDecode);
+ return new String(doFinal);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_DATA_DECRYPTING, e.getMessage());
+ }
+ }
+
+ /**
+ * get iv-buffer
+ *
+ * @param ivSeq ivSequence-Number
+ */
+ private byte[] getIv(int ivSeq) throws TapoErrorHandler {
+ byte[] seq = BigInteger.valueOf(ivSeq).toByteArray();
+ return concatBytes(iv, seq);
+ }
+
+ /************************
+ * INIT CIPHER-SPECS
+ ************************/
+
+ /**
+ * INIT IV-DERIVE FROM SEEDS
+ */
+ private byte[] initIvDerive(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+ throws TapoErrorHandler, NoSuchAlgorithmException {
+ /* iv is first 16 bytes of sha256, where the last 4 bytes forms the */
+ /* sequence number used in requests and is incremented on each request */
+ byte[] fullBytes = concatBytes("iv".getBytes(), localSeed, remoteSeed, userHash);
+ byte[] byteHash = sha256Encode(fullBytes);
+ byte[] seqBytes = truncateByteArray(byteHash, byteHash.length - 4, 4);
+
+ ivSeq = new BigInteger(seqBytes).intValue(); /* get sequence number */
+ return truncateByteArray(byteHash, 0, 12);
+ }
+
+ /**
+ * INIT KEY-DERIVE FROM SEEDS
+ */
+ private byte[] initKeyDerive(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+ throws TapoErrorHandler, NoSuchAlgorithmException {
+ byte[] fullBytes = concatBytes("lsk".getBytes(), localSeed, remoteSeed, userHash);
+ byte[] byteHash = sha256Encode(fullBytes);
+ return truncateByteArray(byteHash, 0, 16);
+ }
+
+ /**
+ * INIT SIGNATURE-DERIVE FROM SEEDS
+ */
+ private byte[] initSigDerive(byte[] localSeed, byte[] remoteSeed, byte[] userHash)
+ throws TapoErrorHandler, NoSuchAlgorithmException {
+ /* used to create a hash with which to prefix each request */
+ byte[] fullBytes = concatBytes("ldk".getBytes(), localSeed, remoteSeed, userHash);
+ byte[] byteHash = sha256Encode(fullBytes);
+ return truncateByteArray(byteHash, 0, 28);
+ }
+
+ /************************
+ * HELPERS
+ ************************/
+
+ /**
+ * Return initialized Cipher
+ *
+ * @param opMode op-mode (encrypt/decrypt)
+ * @param ivSeq iv-sequence number
+ */
+ private Cipher getCipher(int opMode, int ivSeq) throws TapoErrorHandler {
+ try {
+ byte[] ivBuffer = getIv(ivSeq);
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key, CIPHER_ALGORITHM);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBuffer);
+
+ Cipher myCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+ myCipher.init(opMode, secretKeySpec, ivParameterSpec);
+
+ return myCipher;
+ } catch (Exception e) {
+ throw new TapoErrorHandler(e);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.klap;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO-KLAP-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class KlapProtocol implements org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface {
+
+ private final Logger logger = LoggerFactory.getLogger(KlapProtocol.class);
+ protected final TapoConnectorInterface httpDelegator;
+ private KlapSession session;
+ private String uid;
+
+ /***********************
+ * Init Class
+ **********************/
+ public KlapProtocol(TapoConnectorInterface httpDelegator) {
+ this.httpDelegator = httpDelegator;
+ session = new KlapSession(this);
+ uid = httpDelegator.getThingUID() + " / HTTP-KLAP";
+ }
+
+ @Override
+ public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
+ logger.trace("({}) login to device", uid);
+ session.reset();
+ session.login(tapoCredentials);
+ return isLoggedIn();
+ }
+
+ @Override
+ public void logout() {
+ session.reset();
+ }
+
+ @Override
+ public boolean isLoggedIn() {
+ return session.isHandshakeComplete() && session.seedIsOkay() && !session.isExpired();
+ }
+
+ /***********************
+ * Request Sender
+ **********************/
+
+ /*
+ * send synchron request - response will be handled in [responseReceived()] function
+ */
+ @Override
+ public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler {
+ String url = getUrl();
+ String command = tapoRequest.method();
+ logger.trace("({}) sending unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+
+ Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ /* set header */
+ httpRequest = setHeaders(httpRequest);
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ /* add request body */
+ httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+ try {
+ responseReceived(httpRequest.send(), command);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(e, "error sending content");
+ }
+ }
+
+ /**
+ * handle asynchron request-response
+ * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
+ String url = getUrl();
+ String command = tapoRequest.method();
+ logger.trace("({}) sendAsync unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
+
+ /* encrypt request */
+ byte[] encodedBytes = session.encryptRequest(tapoRequest);
+ String encrypteString = byteArrayToHex(encodedBytes);
+ Integer ivSequence = session.getIvSequence();
+ logger.trace("({}) encrypted request is '{}' with sequence '{}'", uid, encrypteString, ivSequence);
+
+ Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ /* set header and params */
+ httpRequest = setHeaders(httpRequest);
+ httpRequest.param("seq", ivSequence.toString());
+
+ /* add request body */
+ httpRequest.content(new BytesContentProvider(encodedBytes));
+
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
+ @NonNullByDefault({})
+ @Override
+ public void onComplete(Result result) {
+ final HttpResponse response = (HttpResponse) result.getResponse();
+
+ if (result.getFailure() != null) {
+ /* handle result errors */
+ Throwable e = result.getFailure();
+ String errorMessage = getValueOrDefault(e.getMessage(), "");
+ /* throw errors to delegator */
+ if (e instanceof TimeoutException) {
+ logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
+ } else {
+ logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
+ httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
+ }
+ } else if (response.getStatus() != 200) {
+ logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
+ } else {
+ /* request successful */
+ byte[] responseBytes = getContent();
+ try {
+ encryptedResponseReceived(responseBytes, ivSequence, command);
+ } catch (TapoErrorHandler tapoError) {
+ httpDelegator.handleError(tapoError);
+ }
+ }
+ }
+ });
+ }
+
+ /************************
+ * RESPONSE HANDLERS
+ ************************/
+
+ /**
+ * handle synchron request-response
+ * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
+ logger.trace("({}) received response content: '{}'", uid, response.getContentAsString());
+ TapoResponse tapoResponse = getTapoResponse(response);
+ httpDelegator.handleResponse(tapoResponse, command);
+ httpDelegator.responsePasstrough(response.getContentAsString(), command);
+ }
+
+ /**
+ * handle asynchron request-response
+ * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
+ try {
+ TapoResponse tapoResponse = getTapoResponse(content);
+ httpDelegator.handleResponse(tapoResponse, command);
+ } catch (TapoErrorHandler tapoError) {
+ httpDelegator.handleError(tapoError);
+ }
+ }
+
+ /**
+ * handle encrypted response. decrypt it and pass to asyncRequestReceived
+ *
+ * @param content bytearray with encrypted payload
+ * @param ivSeq ivSequence-Number which is incremented each request
+ * @param command command was sent to device
+ * @throws TapoErrorHandler
+ */
+ public void encryptedResponseReceived(byte[] content, Integer ivSeq, String command) throws TapoErrorHandler {
+ String stringContent = byteArrayToHex(content);
+ logger.trace("({}) receivedRespose '{}'", uid, stringContent);
+ String decryptedResponse = session.decryptResponse(content, ivSeq);
+ logger.trace("({}) decrypted response: '{}'", uid, decryptedResponse);
+ asyncResponseReceived(decryptedResponse, command);
+ }
+
+ /**
+ * Get Tapo-Response from Contentresponse
+ * decrypt if is encrypted
+ */
+ protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
+ if (response.getStatus() == 200) {
+ return getTapoResponse(response.getContentAsString());
+ } else {
+ logger.debug("({}) invalid response received", uid);
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /**
+ * Get Tapo-Response from responsestring
+ * decrypt if is encrypted
+ */
+ protected TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
+ if (isValidJson(responseString)) {
+ TapoResponse tapoResponse = Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
+ if (tapoResponse.hasError()) {
+ throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
+ }
+ return tapoResponse;
+ } else {
+ logger.debug("({}) invalid response received", uid);
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /************************
+ * PRIVATE HELPERS
+ ************************/
+
+ protected String getUrl() {
+ String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
+ if (session.isHandshakeComplete()) {
+ return baseUrl + "/request";
+ } else {
+ return baseUrl;
+ }
+ }
+
+ /*
+ * Set HTTP-Headers
+ */
+ protected Request setHeaders(Request httpRequest) {
+ if (!session.isHandshakeComplete()) {
+ httpRequest.header("content-type", CONTENT_TYPE_JSON);
+ httpRequest.header("Accept", CONTENT_TYPE_JSON);
+ }
+ if (!session.getCookie().isBlank()) {
+ httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());
+ }
+ return httpRequest;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.klap;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for KLAP Session
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class KlapSession {
+ private final Logger logger = LoggerFactory.getLogger(KlapSession.class);
+
+ private final KlapProtocol klap;
+ private @NonNullByDefault({}) KlapCipher cipher;
+ private byte[] localSeed = new byte[16];
+ private byte[] remoteSeed = new byte[16];
+ private byte[] serverHash = new byte[0];
+ private byte[] authHash = new byte[0];
+ private byte[] localSeedAuthHash = new byte[0];
+ private String sessionId = "";
+ private long expireAt = 0L;
+ private String uid;
+
+ // List of class-specific commands
+ public static final String DEVICE_CMD_HANDSHAKE1 = "handshake1";
+ public static final String DEVICE_CMD_HANDSHAKE2 = "handshake2";
+ public static final String DEVICE_CMD_SECURE_METHOD = "securePassthrough";
+ public static final String TP_SESSION_COOKIE_HEADER = "Set-Cookie";
+ public static final String TP_SESSION_COOKIE_NAME = "TP_SESSIONID";
+ public static final String TP_SESSION_COOKIE_TIMEOUT = "TIMEOUT";
+
+ /************************
+ * INIT CLASS
+ ************************/
+
+ public KlapSession(KlapProtocol klapHandler) {
+ klap = klapHandler;
+ localSeed = newLocalSeed();
+ uid = klap.httpDelegator.getThingUID() + "KLAP-Session";
+ }
+
+ /* create new random local seed */
+ private byte[] newLocalSeed() {
+ SecureRandom random = new SecureRandom();
+ byte[] randomBytes = new byte[16];
+ random.nextBytes(randomBytes);
+ return randomBytes;
+ }
+
+ /************************
+ * SET VALUES
+ ************************/
+
+ /* reset data (logout) */
+ public void reset() {
+ logger.trace("reset KlapSession");
+ localSeed = newLocalSeed();
+ remoteSeed = new byte[0];
+ localSeedAuthHash = new byte[0];
+ serverHash = new byte[0];
+ sessionId = "";
+ expireAt = 0L;
+ cipher = null;
+ }
+
+ /* set sessionId (cookie) */
+ public boolean setSession(ContentResponse response) throws TapoErrorHandler {
+ try {
+ /* get cookie */
+ String header = response.getHeaders().get(TP_SESSION_COOKIE_HEADER);
+ String cookie = header.split(";")[0].replace(TP_SESSION_COOKIE_NAME + "=", "");
+ int timeout = Integer.parseInt(header.split(";")[1].replace(TP_SESSION_COOKIE_TIMEOUT + "=", ""));
+ sessionId = cookie;
+ expireAt = System.currentTimeMillis() * timeout;
+ /* get seeds */
+ byte[] responseContent = response.getContent();
+ byte[] bRemoteSeed = truncateByteArray(responseContent, 0, 16); // get first 16 bytes
+ byte[] bServerHash = truncateByteArray(responseContent, 16, responseContent.length - 16); // get
+ // rest
+ setSeed(bRemoteSeed, bServerHash);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED, e.getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * set Seed and compare handshake1 hashes
+ */
+ public boolean setSeed(byte[] remoteSeed, byte[] serverHash) throws TapoErrorHandler {
+ logger.trace("({}) Init Session", uid);
+ try {
+ logger.trace("remoteseed is '{}' / serverhash is '{}' authhash is '{}'", byteArrayToHex(localSeed),
+ byteArrayToHex(remoteSeed), byteArrayToHex(authHash));
+
+ byte[] concatByteArray = concatBytes(localSeed, remoteSeed, authHash);
+ byte[] bLocalSeedAuthHash = sha256Encode(concatByteArray);
+
+ if (Arrays.equals(bLocalSeedAuthHash, serverHash)) {
+ logger.trace("handshake1 sucessfull");
+ this.remoteSeed = remoteSeed;
+ this.serverHash = serverHash;
+ this.localSeedAuthHash = bLocalSeedAuthHash;
+ return true;
+ } else {
+ logger.trace("handshake1 does not match {} / {}", byteArrayToHex(bLocalSeedAuthHash),
+ byteArrayToHex(serverHash));
+ reset();
+ throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED);
+ }
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED);
+ }
+ }
+
+ /***********************
+ * Request Sender
+ **********************/
+
+ /*
+ * Sender for handling Handshake
+ */
+ private ContentResponse sendHandshake(String customUrl, byte[] payloadBytes, String command)
+ throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+ logger.trace("({}) sending bytes'{} to '{}' ", uid, byteArrayToHex(payloadBytes), customUrl);
+
+ Request httpRequest = klap.httpDelegator.getHttpClient().newRequest(customUrl)
+ .method(HttpMethod.POST.toString());
+
+ httpRequest = klap.setHeaders(httpRequest);
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ /* add request body */
+ httpRequest.content(new BytesContentProvider(payloadBytes));
+
+ return httpRequest.send();
+ }
+
+ /* complete Handshake (init Cipher) */
+ private void completeHandshake() throws Exception {
+ cipher = new KlapCipher(localSeed, remoteSeed, authHash);
+ }
+
+ /************************
+ * HANDSHAKE & COOKIE
+ ************************/
+
+ /**
+ * Create Handshake
+ */
+ public boolean login(TapoCredentials credentials) throws TapoErrorHandler {
+ try {
+ authHash = generateAuthHash(credentials);
+ if (createHandshake1()) {
+ logger.trace("({}) handshake1 successfull", uid);
+ if (createHandshake2()) {
+ logger.trace("({}) handshake2 successfull", uid);
+ completeHandshake();
+ return isHandshakeComplete();
+ }
+ } else {
+ throw new TapoErrorHandler(NO_ERROR, BINDING_ID);
+ }
+ } catch (TapoErrorHandler e) {
+ throw e;
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_API_HAND_SHAKE_FAILED);
+ }
+ return false;
+ }
+
+ /**
+ * Handle Handshake (set session)
+ */
+ private boolean createHandshake1()
+ throws TimeoutException, InterruptedException, ExecutionException, TapoErrorHandler {
+ String handshakeUrl = klap.getUrl() + "/" + DEVICE_CMD_HANDSHAKE1;
+ byte[] bytes = getLocalSeed();
+ ContentResponse response = sendHandshake(handshakeUrl, bytes, DEVICE_CMD_HANDSHAKE1);
+ if (response.getStatus() == 200) {
+ logger.trace("({}) got handshake response", uid);
+ setSession(response);
+ return seedIsOkay();
+ } else {
+ logger.debug("({}) invalid handshake1 response {}", uid, response.getStatus());
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /**
+ * Complete Handshake2 (set cipher)
+ */
+ private boolean createHandshake2() throws TimeoutException, InterruptedException, ExecutionException,
+ NoSuchAlgorithmException, TapoErrorHandler {
+ String handshakeUrl = klap.getUrl() + "/" + DEVICE_CMD_HANDSHAKE2;
+ byte[] byteArr = concatBytes(remoteSeed, localSeed, authHash);
+ byte[] payloadBytes = sha256Encode(byteArr);
+ ContentResponse response = sendHandshake(handshakeUrl, payloadBytes, DEVICE_CMD_HANDSHAKE2);
+
+ if (response.getStatus() == 200) {
+ return true;
+ } else {
+ logger.debug("({}) invalid handshake1 response {}", uid, response.getStatus());
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, response.getReason());
+ }
+ }
+
+ /************************
+ * ENCODING / DECODING
+ ************************/
+
+ /**
+ * generate auth-hash from credentials
+ */
+ private byte[] generateAuthHash(TapoCredentials credentials) throws NoSuchAlgorithmException, TapoErrorHandler {
+ byte[] bUsername = sha1Encode(credentials.username().getBytes(StandardCharsets.UTF_8));
+ byte[] bPassword = sha1Encode(credentials.password().getBytes(StandardCharsets.UTF_8));
+ return sha256Encode(concatBytes(bUsername, bPassword));
+ }
+
+ /**
+ * Decrypt encrypted TapoResponse
+ */
+ public String decryptResponse(byte[] byteResponse, Integer ivSeq) throws TapoErrorHandler {
+ try {
+ return cipher.decrypt(byteResponse, ivSeq);
+ } catch (Exception e) {
+ logger.debug("({}) exception decrypting Payload '{}'", uid, e.toString());
+ throw new TapoErrorHandler(ERR_DATA_DECRYPTING);
+ }
+ }
+
+ /**
+ * Encrypt Request
+ */
+ public byte[] encryptRequest(Object request) throws TapoErrorHandler {
+ try {
+ return cipher.encrypt(request.toString());
+ } catch (Exception e) {
+ logger.debug("({}) exception encrypting Payload '{}'", uid, e.toString());
+ throw new TapoErrorHandler(ERR_DATA_ENCRYPTING);
+ }
+ }
+
+ /************************
+ * GET VALUES
+ ************************/
+
+ public long expireAt() {
+ return expireAt;
+ }
+
+ public String getCookie() {
+ if (!sessionId.isBlank()) {
+ return TP_SESSION_COOKIE_NAME + "=" + sessionId;
+ }
+ return "";
+ }
+
+ public KlapCipher getCipher() {
+ return cipher;
+ }
+
+ public byte[] getLocalSeed() {
+ return localSeed;
+ }
+
+ public Integer getIvSequence() {
+ return cipher.getIvSeq();
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public boolean isHandshakeComplete() {
+ return !sessionId.isBlank() && cipher != null && cipher.isInitialized();
+ }
+
+ public boolean isExpired() {
+ return (expireAt - (System.currentTimeMillis())) <= 40 * 1000;
+ }
+
+ /* return true if seeds are set */
+ public boolean seedIsOkay() {
+ return serverHash.length > 0 && Arrays.equals(localSeedAuthHash, serverHash);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.api.protocol.passthrough;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO-PASSTHROUGH-Protocol
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class PassthroughProtocol implements TapoProtocolInterface {
+ private final Logger logger = LoggerFactory.getLogger(PassthroughProtocol.class);
+ private final TapoConnectorInterface httpDelegator;
+ private final String uid;
+
+ /***********************
+ * Init Class
+ **********************/
+
+ public PassthroughProtocol(TapoConnectorInterface httpDelegator) {
+ this.httpDelegator = httpDelegator;
+ uid = httpDelegator.getThingUID() + " / HTTP-Passtrhough";
+ }
+
+ /***********************
+ * Login Handling
+ **********************/
+
+ @Override
+ public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
+ logger.debug("({}) login not implemented", uid);
+ throw new TapoErrorHandler(ERR_BINDING_NOT_IMPLEMENTED, "NOT NEEDED");
+ }
+
+ @Override
+ public void logout() {
+ logger.trace("({}) logout not implemented", uid);
+ }
+
+ @Override
+ public boolean isLoggedIn() {
+ logger.debug("({}) isLoggedIn not implemented", uid);
+ return false;
+ }
+
+ /***********************
+ * Request Sender
+ **********************/
+
+ /*
+ * send synchronous request - response will be handled in [responseReceived()] function
+ */
+ @Override
+ public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler {
+ String url = getUrl();
+ logger.trace("({}) sending encrypted request to '{}' ", uid, url);
+ logger.trace("({}) unencrypted request: '{}'", uid, tapoRequest);
+
+ Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ /* set header */
+ httpRequest = setHeaders(httpRequest);
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ /* add request body */
+ httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+ try {
+ responseReceived(httpRequest.send(), tapoRequest.method());
+ } catch (TapoErrorHandler tapoError) {
+ logger.debug("({}) sendRequest exception'{}'", uid, tapoError.toString());
+ throw tapoError;
+ } catch (TimeoutException e) {
+ logger.debug("({}) sendRequest timeout'{}'", uid, e.getMessage());
+ throw new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, getValueOrDefault(e.getMessage(), ""));
+ } catch (InterruptedException e) {
+ logger.debug("({}) sendRequest interrupted'{}'", uid, e.getMessage());
+ throw new TapoErrorHandler(ERR_BINDING_SEND_REQUEST, getValueOrDefault(e.getMessage(), ""));
+ } catch (ExecutionException e) {
+ logger.debug("({}) sendRequest exception'{}'", uid, e.getMessage());
+ throw new TapoErrorHandler(ERR_BINDING_SEND_REQUEST, getValueOrDefault(e.getMessage(), ""));
+ }
+ }
+
+ /*
+ * send asynchronous request - response will be handled in [asyncResponseReceived()] function
+ */
+ @Override
+ public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
+ String url = getUrl();
+ String command = tapoRequest.method();
+ logger.trace("({}) sendAsncRequest to '{}'", uid, url);
+ logger.trace("({}) command/payload: '{}''{}'", uid, command, tapoRequest.params());
+
+ Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
+
+ /* set header */
+ httpRequest = setHeaders(httpRequest);
+
+ /* add request body */
+ httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
+
+ httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
+ @NonNullByDefault({})
+ @Override
+ public void onComplete(Result result) {
+ final HttpResponse response = (HttpResponse) result.getResponse();
+ if (result.getFailure() != null) {
+ /* handle result errors */
+ Throwable e = result.getFailure();
+ String errorMessage = getValueOrDefault(e.getMessage(), "");
+ if (e instanceof TimeoutException) {
+ logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
+ } else {
+ logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
+ httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
+ }
+ } else if (response.getStatus() != 200) {
+ logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
+ } else {
+ /* request successful */
+ String rBody = getContentAsString();
+ try {
+ asyncResponseReceived(rBody, command);
+ } catch (TapoErrorHandler tapoError) {
+ httpDelegator.handleError(tapoError);
+ }
+ }
+ }
+ });
+ }
+
+ /************************
+ * RESPONSE HANDLERS
+ ************************/
+
+ /**
+ * handle synchronous request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
+ logger.trace("({}) recived response: {}", uid, response.getContentAsString());
+ TapoResponse tapoResponse = getTapoResponse(response);
+ if (!tapoResponse.hasError()) {
+ switch (command) {
+ default:
+ httpDelegator.handleResponse(tapoResponse, command);
+ httpDelegator.responsePasstrough(response.getContentAsString(), command);
+ }
+ } else {
+ logger.debug("({}) response returned error: {} ({})", uid, tapoResponse.message(),
+ tapoResponse.errorCode());
+ httpDelegator.handleError(new TapoErrorHandler(tapoResponse.errorCode()));
+ }
+ }
+
+ /**
+ * handle asynchronous request-response - pushes TapoResponse to [httpDelegator.handleResponse()]-function
+ */
+ @Override
+ public void asyncResponseReceived(String responseBody, String command) throws TapoErrorHandler {
+ logger.trace("({}) asyncResponseReceived '{}'", uid, responseBody);
+ TapoResponse tapoResponse = getTapoResponse(responseBody);
+ if (!tapoResponse.hasError()) {
+ httpDelegator.handleResponse(tapoResponse, command);
+ } else {
+ httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, command));
+ }
+ }
+
+ /************************
+ *
+ * PRIVATE HELPERS
+ *
+ ************************/
+
+ /**
+ * Get Tapo-Response from Contentresponse
+ */
+ private TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
+ if (response.getStatus() == 200) {
+ return getTapoResponse(response.getContentAsString());
+ } else {
+ logger.debug("({}) invalid response received", uid);
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /**
+ * Get Tapo-Response from responsestring
+ */
+ private TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
+ if (isValidJson(responseString)) {
+ return Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
+ } else {
+ logger.debug("({}) invalid response received", uid);
+ throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
+ }
+ }
+
+ /**
+ * Get Url requests are sent to
+ */
+ public String getUrl() {
+ return httpDelegator.getBaseUrl();
+ }
+
+ /*
+ * Set HTTP-Headers
+ */
+ public Request setHeaders(Request httpRequest) {
+ httpRequest.header("content-type", CONTENT_TYPE_JSON);
+ httpRequest.header("Accept", CONTENT_TYPE_JSON);
+ return httpRequest;
+ }
+}
public static final Integer TAPO_HTTP_TIMEOUT_MS = 5000; // http request timeout
public static final Integer TAPO_HTTP_CLOUD_TIMEOUT_MS = 10000; // http request cloud timeout
public static final Integer TAPO_PING_TIMEOUT_MS = 2000; // ping timeout
- public static final Integer TAPO_REFRESH_MIN_GAP_MS = 5000; // min gap between sending refresh request
+ public static final Integer TAPO_QUERY_MIN_GAP_MS = 1000; // min gap between sending query request
public static final Integer TAPO_SEND_MIN_GAP_MS = 1000; // min gap between sending command request
public static final Integer TAPO_LOGIN_MIN_GAP_MS = 5000; // min gap between sending login request
public static final Integer TAPO_LOGIN_MAX_GAP_M = 1440; // max minutes to relogin to device
- public static final Integer TAPO_DISCOVERY_TIMEOUT_S = 6; // timout device discovery in seconds
- public static final Integer POLLING_MIN_INTERVAL_S = 10; // min polling interval (settings)
+ public static final Integer TAPO_DISCOVERY_TIMEOUT_S = 20; // timout device discovery in seconds
+ public static final Integer POLLING_MIN_INTERVAL_S = 1; // min polling interval (settings)
+ public static final Integer TAPO_MULTI_COMMAND_OFFSET_MS = 100; // Offset between sending multiple commands in ms
// FORMATING CONSTANTS
public static final String IPV4_REGEX = "(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))";
public static final char MAC_DIVISION_CHAR = '-';
-
- // LIST OF DEVICE-COMMANDS
- public static final String DEVICE_CMD_GETINFO = "get_device_info";
- public static final String DEVICE_CMD_SETINFO = "set_device_info";
- public static final String DEVICE_CMD_GETENERGY = "get_energy_usage";
- public static final String DEVICE_CMD_CHILD_DEVICE_LIST = "get_child_device_list";
- public static final String DEVICE_CMD_CONTROL_CHILD = "control_child";
- public static final String DEVICE_CMD_MULTIPLE_REQ = "multipleRequest";
- public static final String DEVICE_CMD_CUSTOM = "custom_command";
- public static final String DEVICE_CMD_SET_LIGHT_FX = "set_dynamic_light_effect_rule_enable";
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.constants;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TapoComConstants} class defines communication constants, which are
+ * used across the whole binding.
+ *
+ * @author Christian Wild - Initial contribution
+ ***/
+@NonNullByDefault
+public class TapoComConstants {
+ // DEVICE JSON STRINGS
+ public static final String JSON_KEY_LIGHTNING_EFFECT_OFF = "off";
+ public static final String DEVICE_REPRESENTATION_PROPERTY = "macAddress";
+
+ // List of Cloud-Commands
+ public static final String CLOUD_CMD_LOGIN = "login";
+ public static final String CLOUD_CMD_GETDEVICES = "getDeviceList";
+
+ // List of Device-Control-Commands
+ public static final String DEVICE_CMD_GETINFO = "get_device_info";
+ public static final String DEVICE_CMD_SETINFO = "set_device_info";
+ public static final String DEVICE_CMD_GETENERGY = "get_energy_usage";
+ public static final String DEVICE_CMD_GETCHILDDEVICELIST = "get_child_device_list";
+ public static final String DEVICE_CMD_CONTROL_CHILD = "control_child";
+ public static final String DEVICE_CMD_MULTIPLE_REQ = "multipleRequest";
+ public static final String DEVICE_CMD_CUSTOM = "custom_command";
+ public static final String DEVICE_CMD_SET_DYNAIMCLIGHT_FX = "set_dynamic_light_effect_rule_enable";
+ public static final String DEVICE_CMD_SET_LIGHT_FX = "set_lighting_effect";
+
+ // Sets
+ public static final Set<String> DEVICE_CMDLIST_QUERY = Set.of(DEVICE_CMD_GETINFO, DEVICE_CMD_GETENERGY,
+ DEVICE_CMD_GETCHILDDEVICELIST);
+ public static final Set<String> DEVICE_CMDLIST_SET = Set.of(DEVICE_CMD_SETINFO, DEVICE_CMD_SET_DYNAIMCLIGHT_FX,
+ DEVICE_CMD_CONTROL_CHILD);
+
+ public static final int LOGIN_RETRIES = 1;
+}
@NonNullByDefault
public enum TapoErrorCode {
NO_ERROR(0),
+ // List of API-Errorcodes from device
ERR_UNKNOWN(-1, TapoErrorType.UNKNOWN),
- ERR_API_SESSION_TIMEOUT(9999, TapoErrorType.COMMUNICATION_RETRY),
+ ERR_API_UNKNOWN_COM_ERROR(9999, TapoErrorType.COMMUNICATION_ERROR),
ERR_API_NULL_TRANSPORT(1000),
ERR_API_REQUEST(1002),
- ERR_API_HAND_SHAKE_FAILED(1100, TapoErrorType.COMMUNICATION_RETRY),
+ ERR_API_PROTOCOL(1003, TapoErrorType.CONFIGURATION_ERROR),
+ ERR_API_HAND_SHAKE_FAILED(1100, TapoErrorType.COMMUNICATION_ERROR),
ERR_API_LOGIN_FAILED(1111),
ERR_API_HTTP_TRANSPORT_FAILED(1112),
ERR_API_MULTI_REQUEST_FAILED(1200),
ERR_CLOUD_TOKEN_EXPIRED(-20651),
// List of Binding-ErrorCodes
+ ERR_BINDING_NOT_IMPLEMENTED(9000),
ERR_BINDING_HTTP_RESPONSE(9001, TapoErrorType.COMMUNICATION_ERROR),
ERR_BINDING_COOKIE(9002, TapoErrorType.COMMUNICATION_ERROR),
ERR_BINDING_CREDENTIALS(9003, TapoErrorType.CONFIGURATION_ERROR),
+ ERR_BINDING_LOGIN(9004, TapoErrorType.CONFIGURATION_ERROR),
ERR_BINDING_DEVICE_OFFLINE(9009, TapoErrorType.COMMUNICATION_ERROR),
ERR_BINDING_CONNECT_TIMEOUT(9010, TapoErrorType.COMMUNICATION_ERROR),
+ ERR_BINDING_SEND_REQUEST(9011, TapoErrorType.COMMUNICATION_ERROR),
+ ERR_BINDING_FX_NOT_FOUND(9020, TapoErrorType.CONFIGURATION_ERROR),
+
+ // List of Data-Error
+ ERR_DATA_ENCRYPTING(9500, TapoErrorType.COMMUNICATION_ERROR),
+ ERR_DATA_DECRYPTING(9501, TapoErrorType.COMMUNICATION_ERROR),
+ ERR_DATA_FORMAT(9505, TapoErrorType.COMMUNICATION_ERROR),
+ ERR_DATA_TRANSORMATION(9506),
// List of Binding-Config-ErrorCodes
ERR_CONFIG_IP(10001, TapoErrorType.CONFIGURATION_ERROR), // ip not set
ERR_CONFIG_CREDENTIALS(10002, TapoErrorType.CONFIGURATION_ERROR), // credentials not set
- ERR_CONFIG_NO_BRIDGE(10003, TapoErrorType.CONFIGURATION_ERROR); // no bridge configured
+ ERR_CONFIG_NO_BRIDGE(10003, TapoErrorType.CONFIGURATION_ERROR), // no bridge configured
+ ERR_CONFIG_PROTOCOL(10004, TapoErrorType.CONFIGURATION_ERROR); // unknown protocol
private Integer code;
private TapoErrorType errorType;
import org.openhab.core.thing.ThingTypeUID;
/**
- * The {@link TapoBindingSettings} class defines common constants, which are
+ * The {@link TapoThingConstants} class defines thing constants, which are
* used across the whole binding.
*
* @author Christian Wild - Initial contribution
/*** LIST OF SUPPORTED DEVICE NAMES ***/
public static final String DEVICE_BRIDGE = "bridge";
+ public static final String DEVICE_H100 = "H100";
public static final String DEVICE_P100 = "P100";
public static final String DEVICE_P105 = "P105";
public static final String DEVICE_P110 = "P110";
public static final String DEVICE_L900 = "L900";
public static final String DEVICE_L920 = "L920";
public static final String DEVICE_L930 = "L930";
+ public static final String DEVICE_T110 = "T110";
+ public static final String DEVICE_T310 = "T310";
+ public static final String DEVICE_T315 = "T315";
public static final String DEVICE_UNIVERSAL = "Test_Device";
/*** LIST OF SUPPORTED DEVICE DESCRIPTIONS ***/
public static final String DEVICE_DESCRIPTION_BRIDGE = "TapoControl Cloud-Login";
- public static final String DEVICE_DESCRIPTION_SMART_PLUG = "SmartPlug";
- public static final String DEVICE_DESCRIPTION_POWER_STRIP = "PowerStrip";
+ public static final String DEVICE_DESCRIPTION_HUB = "SmartHub";
+ public static final String DEVICE_DESCRIPTION_SOCKET = "SmartPlug";
+ public static final String DEVICE_DESCRIPTION_SOCKET_STRIP = "PowerStrip";
public static final String DEVICE_DESCRIPTION_WHITE_BULB = "White-Light-Bulb";
public static final String DEVICE_DESCRIPTION_COLOR_BULB = "Color-Light-Bulb";
public static final String DEVICE_DESCRIPTION_LIGHTSTRIP = "LightStrip";
+ public static final String DEVICE_DESCRIPTION_SMART_CONTACT = "Smart-Contact-Sensor";
+ public static final String DEVICE_DESCRIPTION_MOTION_SENSOR = "Motion-Sensor";
+ public static final String DEVICE_DESCRIPTION_TEMP_SENSOR = "Temperature-Sensor";
/*** LIST OF SUPPORTED THING UIDS ***/
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_BRIDGE);
+ public static final ThingTypeUID H100_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_H100);
public static final ThingTypeUID P100_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P100);
public static final ThingTypeUID P105_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P105);
public static final ThingTypeUID P110_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_P110);
public static final ThingTypeUID L930_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_L930);
public static final ThingTypeUID UNIVERSAL_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_UNIVERSAL);
+ /*** LIST OF SUPPORTED HUB CHILD THING UIDS ***/
+ public static final ThingTypeUID T110_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_T110);
+ public static final ThingTypeUID T310_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_T310);
+ public static final ThingTypeUID T315_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_T315);
+
/*** SET OF SUPPORTED UIDS ***/
public static final Set<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,
- P110_THING_TYPE, P115_THING_TYPE, P300_THING_TYPE);
+ public static final Set<ThingTypeUID> SUPPORTED_HUB_UIDS = Set.of(H100_THING_TYPE);
+ public static final Set<ThingTypeUID> SUPPORTED_SOCKET_UIDS = Set.of(P100_THING_TYPE, P105_THING_TYPE,
+ P110_THING_TYPE, P115_THING_TYPE);
+ public static final Set<ThingTypeUID> SUPPORTED_SOCKET_STRIP_UIDS = Set.of(P300_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_WHITE_BULB_UIDS = Set.of(L510_THING_TYPE, L610_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_COLOR_BULB_UIDS = Set.of(L530_THING_TYPE, L630_THING_TYPE);
public static final Set<ThingTypeUID> SUPPORTED_LIGHT_STRIP_UIDS = Set.of(L900_THING_TYPE, L920_THING_TYPE,
L930_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()));
+ public static final Set<ThingTypeUID> SUPPORTED_HUB_CHILD_TYPES_UIDS = Set.of(T110_THING_TYPE, T310_THING_TYPE,
+ T315_THING_TYPE);
+ public static final Set<ThingTypeUID> SUPPORTED_SMART_CONTACTS = Set.of(T110_THING_TYPE);
+ public static final Set<ThingTypeUID> SUPPORTED_MOTION_SENSORS = Set.of();
+ public static final Set<ThingTypeUID> SUPPORTED_WHEATHER_SENSORS = Set.of(T310_THING_TYPE, T315_THING_TYPE);
+
+ /*** SET OF ALL SUPPORTED THINGS ***/
+ public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream
+ .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_HUB_UIDS, SUPPORTED_SOCKET_UIDS, SUPPORTED_SOCKET_STRIP_UIDS,
+ SUPPORTED_WHITE_BULB_UIDS, SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS,
+ SUPPORTED_SMART_CONTACTS, SUPPORTED_MOTION_SENSORS, SUPPORTED_WHEATHER_SENSORS)
+ .flatMap(Set::stream).collect(Collectors.toSet()));
+
/*** THINGS WITH ENERGY DATA ***/
public static final Set<ThingTypeUID> SUPPORTED_ENERGY_DATA_UIDS = Set.of(P110_THING_TYPE, P115_THING_TYPE);
- /*** THINGS WITH CHILDS DATA ***/
- public static final Set<ThingTypeUID> SUPPORTED_CHILDS_DATA_UIDS = Set.of(P300_THING_TYPE);
-
/*** 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 JSON STRINGS (CLOUD) ***/
- public static final String CLOUD_JSON_KEY_ALIAS = "alias";
- public static final String CLOUD_JSON_KEY_FW = "fwVer";
- public static final String CLOUD_JSON_KEY_HW = "deviceHwVer";
- public static final String CLOUD_JSON_KEY_ID = "deviceId";
- public static final String CLOUD_JSON_KEY_MAC = "deviceMac";
- public static final String CLOUD_JSON_KEY_MODEL = "deviceName"; // use name cause modell returns different values
- public static final String CLOUD_JSON_KEY_NAME = "deviceName";
- public static final String CLOUD_JSON_KEY_REGION = "deviceRegion";
- public static final String CLOUD_JSON_KEY_SERVER_URL = "appServerUrl";
- public static final String CLOUD_JSON_KEY_TYPE = "deviceType";
-
- /*** DEVICE JSON STRINGS (DEVICE) ***/
- public static final String JSON_KEY_BRIGHTNESS = "brightness";
- public static final String JSON_KEY_COLORTEMP = "color_temp";
- public static final String JSON_KEY_FW = "fw_ver";
- public static final String JSON_KEY_HUE = "hue";
- public static final String JSON_KEY_HW_VER = "hw_ver";
- public static final String JSON_KEY_ID = "device_id";
- public static final String JSON_KEY_IP = "ip";
- public static final String JSON_KEY_MAC = "mac";
- public static final String JSON_KEY_MODEL = "model";
- public static final String JSON_KEY_NICKNAME = "nickname";
- public static final String JSON_KEY_ON = "device_on";
- public static final String JSON_KEY_ONTIME = "on_time";
- public static final String JSON_KEY_OVERHEAT = "overheated";
- public static final String JSON_KEY_REGION = "region";
- public static final String JSON_KEY_SATURATION = "saturation";
- public static final String JSON_KEY_SIGNAL_LEVEL = "signal_level";
- public static final String JSON_KEY_RSSI = "rssi";
- public static final String JSON_KEY_TYPE = "type";
- public static final String JSON_KEY_USAGE_7 = "time_usage_past7";
- public static final String JSON_KEY_USAGE_30 = "time_usage_past30";
- public static final String JSON_KEY_USAGE_TODAY = "time_usage_today";
- public static final String DEVICE_REPRESENTATION_PROPERTY = "macAddress";
- // lightning effects
- public static final String JSON_KEY_LIGHTNING_EFFECT = "lighting_effect";
- public static final String JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS = "brightness";
- public static final String JSON_KEY_LIGHTNING_EFFECT_COLORTEMPRANGE = "color_temp_range";
- public static final String JSON_KEY_LIGHTNING_EFFECT_CUSTOM = "custom";
- public static final String JSON_KEY_LIGHTNING_EFFECT_OFF = "off";
- public static final String JSON_KEY_LIGHTNING_EFFECT_DISPLAYCOLORS = "displayColors";
- public static final String JSON_KEY_LIGHTNING_EFFECT_ENABLE = "enable";
- public static final String JSON_KEY_LIGHTNING_EFFECT_ID = "id";
- public static final String JSON_KEY_LIGHTNING_EFFECT_NAME = "name";
- public static final String JSON_KEY_LIGHTNING_DYNAMIC_ENABLE = "dynamic_light_effect_enable";
- public static final String JSON_KEY_LIGHTNING_DYNAMIC_ID = "dynamic_light_effect_id";
-
- // energy monitoring
- public static final String JSON_KEY_ENERGY_POWER = "current_power";
- public static final String JSON_KEY_ENERGY_RUNTIME_TODAY = "today_runtime";
- public static final String JSON_KEY_ENERGY_RUNTIME_MONTH = "month_runtime";
- public static final String JSON_KEY_ENERGY_ENERGY_TODAY = "today_energy";
- public static final String JSON_KEY_ENERGY_ENERGY_MONTH = "month_energy";
- public static final String JSON_KEY_ENERGY_PAST24H = "past24h";
- public static final String JSON_KEY_ENERGY_PAST7D = "past7d";
- public static final String JSON_KEY_ENERGY_PAST30D = "past30d";
- public static final String JSON_KEY_ENERGY_PAST1Y = "past1y";
- // childs management
- public static final String JSON_KEY_CHILD_START_INDEX = "start_index";
+ public static final Set<ThingTypeUID> CHANNEL_GROUP_THING_SET = Collections.unmodifiableSet(Stream
+ .of(SUPPORTED_BRIDGE_UIDS, SUPPORTED_HUB_UIDS, SUPPORTED_SOCKET_UIDS, SUPPORTED_SOCKET_STRIP_UIDS,
+ SUPPORTED_WHITE_BULB_UIDS, SUPPORTED_COLOR_BULB_UIDS, SUPPORTED_LIGHT_STRIP_UIDS,
+ SUPPORTED_SMART_CONTACTS, SUPPORTED_MOTION_SENSORS, SUPPORTED_WHEATHER_SENSORS)
+ .flatMap(Set::stream).collect(Collectors.toSet()));
+
+ public static final String CHILD_REPRESENTATION_PROPERTY = "serialNumber";
/*** DEVICE SETTINGS ***/
public static final Integer BULB_MIN_COLORTEMP = 2500;
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_TEMP = "colorTemperature";
+ public static final String CHANNEL_MODE = "mode";
public static final String CHANNEL_OUTPUT = "output";
public static final String CHANNEL_SWITCH = "switch";
// channel group device
public static final String CHANNEL_GROUP_DEVICE = "device";
+ public static final String CHANNEL_BATTERY_LOW = "batteryLow";
public static final String CHANNEL_ONTIME = "onTime";
public static final String CHANNEL_OVERHEAT = "overheated";
+ public static final String CHANNEL_SIGNAL_STRENGTH = "signalStrength";
public static final String CHANNEL_WIFI_STRENGTH = "wifiSignal";
+ // channel group alarm
+ public static final String CHANNEL_GROUP_ALARM = "alarm";
+ public static final String CHANNEL_ALARM_ACTIVE = "alarmActive";
+ public static final String CHANNEL_ALARM_SOURCE = "alarmSource";
+ // channel group sensor
+ public static final String CHANNEL_GROUP_SENSOR = "sensor";
+ public static final String CHANNEL_IS_OPEN = "isOpen";
+ public static final String CHANNEL_TEMPERATURE = "currentTemp";
+ public static final String CHANNEL_HUMIDITY = "currentHumidity";
// channel group energy monitor
public static final String CHANNEL_GROUP_ENERGY = "energy";
public static final String CHANNEL_NRG_POWER = "actualPower";
public static final String CHANNEL_NRG_USAGE_TODAY = "todayEnergyUsage";
public static final String CHANNEL_NRG_RUNTIME_TODAY = "todayRuntime";
+ public static final String CHANNEL_NRG_USAGE_MONTH = "monthEnergyUsage";
+ public static final String CHANNEL_NRG_RUNTIME_MONTH = "monthRuntime";
// channel group effect
public static final String CHANNEL_GROUP_EFFECTS = "effects";
public static final String CHANNEL_FX_BRIGHTNESS = "fxBrightness";
public static final String PROPERTY_FAMILY = "deviceFamily";
public static final String PROPERTY_LOCATION = "location";
public static final String PROPERTY_WIFI_LEVEL = "signal-strength";
+
+ /*** EVENT LISTS ***/
+ // hub child events
+ public static final String EVENT_BATTERY_LOW = "batteryIsLow";
+ public static final String EVENT_CONTACT_OPENED = "contactOpened";
+ public static final String EVENT_CONTACT_CLOSED = "contactClosed";
+ public static final String EVENT_STATE_BATTERY_LOW = "batteryLow";
+ public static final String EVENT_STATE_OPENED = "open";
+ public static final String EVENT_STATE_CLOSED = "closed";
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import java.util.Collection;
-import java.util.Set;
-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 TapoBridgeConfiguration config = new TapoBridgeConfiguration();
- 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.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 = getConfigAs(TapoBridgeConfiguration.class);
- 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 Set.of(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() {
- int pollingInterval = config.reconnectInterval;
- TimeUnit timeUnit = TimeUnit.MINUTES;
- if (pollingInterval > 0) {
- logger.debug("{} starting cloudScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
-
- this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
- timeUnit);
- } else {
- logger.debug("({}) cloudScheduler disabled with config '0'", uid);
- stopScheduler(this.pollingJob);
- }
- }
-
- /**
- * Start DeviceDiscovery Scheduler
- */
- protected void startDiscoveryScheduler() {
- int pollingInterval = config.discoveryInterval;
- TimeUnit timeUnit = TimeUnit.MINUTES;
- if (config.cloudDiscovery && pollingInterval > 0) {
- logger.debug("{} starting discoveryScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
-
- this.discoveryJob = scheduler.scheduleWithFixedDelay(this::discoverDevices, 0, pollingInterval, timeUnit);
- } else {
- logger.debug("({}) discoveryScheduler disabled with config '0'", uid);
- stopScheduler(this.discoveryJob);
- }
- }
-
- /**
- * Stop scheduler
- *
- * @param scheduler {@code ScheduledFeature<?>} which schould be stopped
- */
- 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.cloudDiscovery) {
- logger.trace("{} discover devicelist from cloud", this.uid);
- deviceList = getDeviceListCloud();
- } else {
- logger.info("{} Discovery disabled in bridge settings ", this.uid);
- }
- return deviceList;
- }
-
- /**
- * GET DEVICELIST FROM CLOUD
- * returns all devices stored in cloud
- *
- * @return deviceList from cloud
- */
- private JsonArray getDeviceListCloud() {
- logger.trace("{} getDeviceList from cloud", this.uid);
- bridgeError.reset(); // reset ErrorHandler
- JsonArray deviceList = new JsonArray();
- if (loginCloud()) {
- deviceList = this.cloudConnector.getDeviceList();
- }
- return deviceList;
- }
-
- /***********************************
- *
- * BRIDGE GETTERS
- *
- ************************************/
-
- public TapoCredentials getCredentials() {
- return this.credentials;
- }
-
- public HttpClient getHttpClient() {
- return this.httpClient;
- }
-
- public ThingUID getUID() {
- return getThing().getUID();
- }
-
- public TapoBridgeConfiguration getBridgeConfig() {
- return this.config;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
-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.constants.TapoErrorCode;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
-import org.openhab.binding.tapocontrol.internal.structures.TapoChildData;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceConfiguration;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoEnergyData;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.Bridge;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-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 = new TapoDeviceConfiguration();
- 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.deviceInfo = new TapoDeviceInfo();
- this.uid = getThing().getUID().getAsString();
- }
-
- /***********************************
- *
- * INIT AND SETTINGS
- *
- ************************************/
-
- /**
- * INITIALIZE DEVICE
- */
- @Override
- public void initialize() {
- try {
- this.config = getConfigAs(TapoDeviceConfiguration.class);
- Bridge bridgeThing = getBridge();
- if (bridgeThing != null) {
- BridgeHandler bridgeHandler = bridgeThing.getHandler();
- if (bridgeHandler != null) {
- this.bridge = (TapoBridgeHandler) bridgeHandler;
- this.connector = new TapoDeviceConnector(this, bridge);
- }
- }
- } 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);
- }
-
- /**
- * CHECK SETTINGS
- *
- * @return TapoErrorHandler with configuration-errors
- */
- protected TapoErrorHandler checkSettings() {
- TapoErrorHandler configErr = new TapoErrorHandler();
-
- /* check bridge */
- if (!(bridge instanceof TapoBridgeHandler)) {
- configErr.raiseError(ERR_CONFIG_NO_BRIDGE);
- return configErr;
- }
- /* check ip-address */
- if (!config.ipAddress.matches(IPV4_REGEX)) {
- configErr.raiseError(ERR_CONFIG_IP);
- return configErr;
- }
- /* check credentials */
- if (!bridge.getCredentials().areSet()) {
- configErr.raiseError(ERR_CONFIG_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();
- startPollingScheduler();
- }
-
- /**
- * Start scheduler
- */
- protected void startPollingScheduler() {
- int pollingInterval = this.config.pollingInterval;
- TimeUnit timeUnit = TimeUnit.SECONDS;
-
- if (pollingInterval > 0) {
- if (pollingInterval < POLLING_MIN_INTERVAL_S) {
- pollingInterval = POLLING_MIN_INTERVAL_S;
- }
- logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
- this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
- pollingInterval, timeUnit);
- } else {
- logger.debug("({}) scheduler disabled with config '0'", uid);
- stopScheduler(this.pollingJob);
- }
- }
-
- /**
- * Stop scheduler
- *
- * @param scheduler {@code ScheduledFeature<?>} which schould be stopped
- */
- protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
- if (scheduler != null) {
- scheduler.cancel(true);
- scheduler = null;
- }
- }
-
- /**
- * Scheduler Action
- */
- protected void pollingSchedulerAction() {
- logger.trace("({}) schedulerAction", uid);
- queryDeviceInfo();
- }
-
- /***********************************
- *
- * ERROR HANDLER
- *
- ************************************/
- /**
- * return device Error
- *
- * @return
- */
- public TapoErrorHandler getErrorHandler() {
- return this.deviceError;
- }
-
- public TapoErrorCode getError() {
- return this.deviceError.getError();
- }
-
- /**
- * 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_REPRESENTATION_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);
- // query energy usage
- if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
- connector.getEnergyUsage();
- }
- // query childs data
- if (SUPPORTED_CHILDS_DATA_UIDS.contains(getThing().getThingTypeUID())) {
- connector.queryChildDevices();
- }
- } 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");
- }
- }
-
- /**
- * Set Device EnergyData to device
- *
- * @param energyData
- */
- public void setEnergyData(TapoEnergyData energyData) {
- publishState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_POWER),
- getPowerType(energyData.getCurrentPower(), Units.WATT));
- publishState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_USAGE_TODAY),
- getEnergyType(energyData.getTodayEnergy(), Units.WATT_HOUR));
- publishState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_RUNTIME_TODAY),
- getTimeType(energyData.getTodayRuntime(), Units.MINUTE));
- }
-
- /**
- * Set Device Child data to device
- *
- * @param hostData
- */
- public void setChildData(TapoChildData hostData) {
- hostData.getChildDeviceList().forEach(child -> {
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT + Integer.toString(child.getPosition())),
- getOnOffType(child.getDeviceOn()));
- });
- }
-
- /**
- * 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 deviceInfo
- */
- 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) {
- queryDeviceInfo(true);
- } 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();
- TapoErrorCode errorCode = deviceError.getError();
-
- if (errorCode == TapoErrorCode.NO_ERROR) {
- if (deviceState != ThingStatus.ONLINE) {
- updateStatus(ThingStatus.ONLINE);
- }
- } else {
- switch (errorCode.getType()) {
- case COMMUNICATION_RETRY:
- connect();
- break;
- case COMMUNICATION_ERROR:
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
- disconnect();
- break;
- case CONFIGURATION_ERROR:
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
- break;
- default:
- 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 + "#", "");
- channel = channel.replace(CHANNEL_GROUP_ENERGY + "#", "");
- return channel;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoLightEffect;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonObject;
-
-/**
- * TAPO Smart-Plug-Device.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoLightStrip extends TapoDevice {
- private final Logger logger = LoggerFactory.getLogger(TapoLightStrip.class);
-
- /**
- * Constructor
- *
- * @param thing Thing object representing device
- */
- public TapoLightStrip(Thing thing) {
- super(thing);
- }
-
- /**
- * handle command sent to device
- *
- * @param channelUID channelUID command is sent to
- * @param command command to be sent
- */
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- Boolean refreshInfo = false;
-
- String channel = channelUID.getIdWithoutGroup();
- String group = channelUID.getGroupId();
- if (command instanceof RefreshType) {
- refreshInfo = true;
- } else if (group == CHANNEL_GROUP_EFFECTS) {
- setLightEffect(channel, command);
- refreshInfo = true;
- } else {
- switch (channel) {
- case CHANNEL_OUTPUT:
- connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
- refreshInfo = true;
- break;
- case CHANNEL_BRIGHTNESS:
- if (command instanceof PercentType percentCommand) {
- Float percent = percentCommand.floatValue();
- setBrightness(percent.intValue()); // 0..100% = 0..100
- refreshInfo = true;
- } else if (command instanceof DecimalType decimalCommand) {
- setBrightness(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR_TEMP:
- if (command instanceof DecimalType decimalCommand) {
- setColorTemp(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR:
- if (command instanceof HSBType hsbCommand) {
- setColor(hsbCommand);
- refreshInfo = true;
- }
- break;
- default:
- logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
- channelUID.getId());
- }
- }
-
- /* refreshInfo */
- if (refreshInfo) {
- queryDeviceInfo(true);
- }
- }
-
- /**
- * SET BRIGHTNESS
- *
- * @param newBrightness percentage 0-100 of new brightness
- */
- protected void setBrightness(Integer newBrightness) {
- /* switch off if 0 */
- if (newBrightness == 0) {
- connector.sendDeviceCommand(JSON_KEY_ON, false);
- } else {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
- connector.sendDeviceCommands(newState);
- }
- }
-
- /**
- * SET COLOR
- *
- * @param command
- */
- protected void setColor(HSBType command) {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_HUE, command.getHue().intValue());
- newState.put(JSON_KEY_SATURATION, command.getSaturation().intValue());
- newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness().intValue());
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * SET COLORTEMP
- *
- * @param colorTemp (Integer) in Kelvin
- */
- protected void setColorTemp(Integer colorTemp) {
- HashMap<String, Object> newState = new HashMap<>();
- colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_COLORTEMP, colorTemp);
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * set Light Effect from channel/command
- *
- * @param channel channel (effect) to set
- * @param command command (value) to set
- */
- protected void setLightEffect(String channel, Command command) {
- TapoLightEffect lightEffect = deviceInfo.getLightEffect();
- switch (channel) {
- case CHANNEL_FX_BRIGHTNESS:
- if (command instanceof PercentType percentCommand) {
- Float percent = percentCommand.floatValue();
- lightEffect.setBrightness(percent.intValue()); // 0..100% = 0..100
- } else if (command instanceof DecimalType decimalCommand) {
- lightEffect.setBrightness(decimalCommand.intValue());
- }
- break;
- case CHANNEL_FX_COLORS:
- // comming soon
- break;
- case CHANNEL_FX_NAME:
- lightEffect.setName(command.toString());
- break;
- }
- setLightEffects(lightEffect);
- }
-
- /**
- * SET LIGHTNING EFFECTS
- *
- * @param lightEffect new lightEffect
- */
- protected void setLightEffects(TapoLightEffect lightEffect) {
- JsonObject newEffect = new JsonObject();
- newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_ENABLE, lightEffect.getEnable());
- newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_NAME, lightEffect.getName());
- newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS, lightEffect.getBrightness());
- newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_COLORTEMPRANGE, lightEffect.getColorTempRange().toString());
- newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_DISPLAYCOLORS, lightEffect.getDisplayColors().toString());
- newEffect.addProperty(JSON_KEY_LIGHTNING_EFFECT_CUSTOM, lightEffect.getCustom());
-
- connector.sendDeviceCommand(JSON_KEY_LIGHTNING_EFFECT, newEffect.toString());
- }
-
- /**
- * UPDATE PROPERTIES
- *
- * @param deviceInfo TapoDeviceInfo
- */
- @Override
- protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
- TapoLightEffect lightEffect = deviceInfo.getLightEffect();
- super.devicePropertiesChanged(deviceInfo);
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
- getPercentType(deviceInfo.getBrightness()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
- getDecimalType(deviceInfo.getColorTemp()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
- getDecimalType(deviceInfo.getSignalLevel()));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
- getTimeType(deviceInfo.getOnTime(), Units.SECOND));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
- // light effect
- publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_BRIGHTNESS),
- getPercentType(lightEffect.getBrightness()));
- publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(lightEffect.getName()));
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.binding.tapocontrol.internal.structures.TapoLightEffect;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Smart-Plug-Device.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoSmartBulb extends TapoDevice {
- private final Logger logger = LoggerFactory.getLogger(TapoSmartBulb.class);
-
- /**
- * Constructor
- *
- * @param thing Thing object representing device
- */
- public TapoSmartBulb(Thing thing) {
- super(thing);
- }
-
- /**
- * handle command sent to device
- *
- * @param channelUID channelUID command is sent to
- * @param command command to be sent
- */
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- Boolean refreshInfo = false;
-
- String channel = channelUID.getIdWithoutGroup();
- if (command instanceof RefreshType) {
- refreshInfo = true;
- } else {
- switch (channel) {
- case CHANNEL_OUTPUT:
- connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
- refreshInfo = true;
- break;
- case CHANNEL_BRIGHTNESS:
- if (command instanceof PercentType percentCommand) {
- Float percent = percentCommand.floatValue();
- setBrightness(percent.intValue()); // 0..100% = 0..100
- refreshInfo = true;
- } else if (command instanceof DecimalType decimalCommand) {
- setBrightness(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR_TEMP:
- if (command instanceof DecimalType decimalCommand) {
- setColorTemp(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR:
- if (command instanceof HSBType hsbCommand) {
- setColor(hsbCommand);
- refreshInfo = true;
- }
- break;
- case CHANNEL_FX_NAME:
- setLightEffect(command.toString());
- break;
- default:
- logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
- channelUID.getId());
- }
- }
-
- /* refreshInfo */
- if (refreshInfo) {
- queryDeviceInfo(true);
- }
- }
-
- /**
- * SET BRIGHTNESS
- *
- * @param newBrightness percentage 0-100 of new brightness
- */
- protected void setBrightness(Integer newBrightness) {
- /* switch off if 0 */
- if (newBrightness == 0) {
- connector.sendDeviceCommand(JSON_KEY_ON, false);
- } else {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
- connector.sendDeviceCommands(newState);
- }
- }
-
- /**
- * SET COLOR
- *
- * @param command
- */
- protected void setColor(HSBType command) {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_HUE, command.getHue().intValue());
- newState.put(JSON_KEY_SATURATION, command.getSaturation().intValue());
- newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness().intValue());
- newState.put(JSON_KEY_LIGHTNING_DYNAMIC_ENABLE, false);
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * SET COLORTEMP
- *
- * @param colorTemp (Integer) in Kelvin
- */
- protected void setColorTemp(Integer colorTemp) {
- HashMap<String, Object> newState = new HashMap<>();
- colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_COLORTEMP, colorTemp);
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * Set light effect
- *
- * @param fxName (String) id of LightEffect
- */
- protected void setLightEffect(String fxName) {
- HashMap<String, Object> newState = new HashMap<>();
- if (fxName.length() > 0 && !fxName.equals(JSON_KEY_LIGHTNING_EFFECT_OFF)) {
- newState.put(JSON_KEY_LIGHTNING_EFFECT_ENABLE, true);
- newState.put(JSON_KEY_LIGHTNING_EFFECT_ID, fxName);
- } else {
- newState.put(JSON_KEY_LIGHTNING_EFFECT_ENABLE, false);
- }
- connector.sendDeviceCommands(DEVICE_CMD_SET_LIGHT_FX, newState);
- }
-
- /**
- * UPDATE PROPERTIES
- *
- * @param deviceInfo TapoDeviceInfo
- */
- @Override
- protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
- super.devicePropertiesChanged(deviceInfo);
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
- getPercentType(deviceInfo.getBrightness()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
- getDecimalType(deviceInfo.getColorTemp()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
- getDecimalType(deviceInfo.getSignalLevel()));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
- getTimeType(deviceInfo.getOnTime(), Units.SECOND));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
-
- updateLightEffectChannels(deviceInfo.getLightEffect());
- }
-
- /**
- * Set light effect channels
- *
- * @param lightEffect
- */
- protected void updateLightEffectChannels(TapoLightEffect lightEffect) {
- String fxId = "";
- if (lightEffect.getEnable().equals(true)) {
- fxId = lightEffect.getId();
- }
- publishState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(fxId));
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Smart-Plug-Device.
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoSmartPlug extends TapoDevice {
- private final Logger logger = LoggerFactory.getLogger(TapoSmartPlug.class);
-
- /**
- * Constructor
- *
- * @param thing Thing object representing device
- */
- public TapoSmartPlug(Thing thing) {
- super(thing);
- }
-
- /**
- * handle command sent to device
- *
- * @param channelUID channelUID command is sent to
- * @param command command to be sent
- */
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- boolean refreshInfo = false;
- String id = channelUID.getIdWithoutGroup();
-
- /* perform actions */
- if (command instanceof RefreshType) {
- refreshInfo = true;
- } else if (command instanceof OnOffType) {
- Boolean targetState = command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE;
- if (CHANNEL_OUTPUT.equals(id)) { // Command is sent to the device output
- connector.sendDeviceCommand(JSON_KEY_ON, targetState);
- refreshInfo = true;
- } else if (id.startsWith(CHANNEL_OUTPUT)) { // Command is sent to a child's device output
- Integer index = Integer.valueOf(id.replace(CHANNEL_OUTPUT, ""));
- connector.sendChildCommand(index, JSON_KEY_ON, targetState);
- refreshInfo = true;
- }
- } else {
- logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
- }
-
- /* refreshInfo */
- if (refreshInfo) {
- queryDeviceInfo(true);
- }
- }
-
- /**
- * UPDATE PROPERTIES
- *
- * @param deviceInfo TapoDeviceInfo
- */
- @Override
- protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
- super.devicePropertiesChanged(deviceInfo);
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
- getDecimalType(deviceInfo.getSignalLevel()));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
- getTimeType(deviceInfo.getOnTime(), Units.SECOND));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceInfo.isOverheated()));
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Universal-Device
- * universal device for testing pruposes
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoUniversalDevice extends TapoDevice {
- private final Logger logger = LoggerFactory.getLogger(TapoUniversalDevice.class);
-
- // CHANNEL LIST
- public static final String CHANNEL_GROUP_DEBUG = "debug";
- public static final String CHANNEL_RESPONSE = "deviceResponse";
- public static final String CHANNEL_COMMAND = "deviceCommand";
-
- /**
- * Constructor
- *
- * @param thing Thing object representing device
- */
- public TapoUniversalDevice(Thing thing) {
- super(thing);
- }
-
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command.toString(), channelUID.getId());
- Boolean refreshInfo = false;
-
- String channel = channelUID.getIdWithoutGroup();
- if (command instanceof RefreshType) {
- refreshInfo = true;
- } else {
- switch (channel) {
- case CHANNEL_OUTPUT:
- connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
- refreshInfo = true;
- break;
- case CHANNEL_BRIGHTNESS:
- if (command instanceof PercentType percentCommand) {
- Float percent = percentCommand.floatValue();
- setBrightness(percent.intValue()); // 0..100% = 0..100
- refreshInfo = true;
- } else if (command instanceof DecimalType decimalCommand) {
- setBrightness(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR_TEMP:
- if (command instanceof DecimalType decimalCommand) {
- setColorTemp(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR:
- if (command instanceof HSBType hsbCommand) {
- setColor(hsbCommand);
- refreshInfo = true;
- }
- break;
- case CHANNEL_COMMAND:
- String[] cmd = command.toString().split(":");
- if (cmd.length == 1) {
- connector.sendCustomQuery(cmd[0]);
- } else if (cmd.length == 2) {
- connector.sendDeviceCommand(cmd[0], cmd[1]);
- } else {
- logger.warn("({}) wrong command format '{}'", uid, command.toString());
- }
- break;
- default:
- logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
- channelUID.getId());
- }
- }
-
- /* refreshInfo */
- if (refreshInfo) {
- queryDeviceInfo();
- }
- }
-
- /**
- * SET BRIGHTNESS
- *
- * @param newBrightness percentage 0-100 of new brightness
- */
- protected void setBrightness(Integer newBrightness) {
- /* switch off if 0 */
- if (newBrightness == 0) {
- connector.sendDeviceCommand(JSON_KEY_ON, false);
- } else {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
- connector.sendDeviceCommands(newState);
- }
- }
-
- /**
- * SET COLOR
- *
- * @param command
- */
- protected void setColor(HSBType command) {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_HUE, command.getHue());
- newState.put(JSON_KEY_SATURATION, command.getSaturation());
- newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness());
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * SET COLORTEMP
- *
- * @param colorTemp (Integer) in Kelvin
- */
- protected void setColorTemp(Integer colorTemp) {
- HashMap<String, Object> newState = new HashMap<>();
- colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_COLORTEMP, colorTemp);
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * SET DEVICE INFOs to device
- *
- * @param deviceInfo
- */
- @Override
- public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
- devicePropertiesChanged(deviceInfo);
- handleConnectionState();
- }
-
- /**
- * Handle full responsebody received from connector
- *
- * @param responseBody
- */
- @Override
- public void responsePasstrough(String responseBody) {
- logger.debug("({}) received response {}", uid, responseBody);
- publishState(getChannelID(CHANNEL_GROUP_DEBUG, CHANNEL_RESPONSE), getStringType(responseBody));
- }
-
- /**
- * UPDATE PROPERTIES
- *
- * @param deviceInfo TapoDeviceInfo
- */
- @Override
- protected void devicePropertiesChanged(TapoDeviceInfo deviceInfo) {
- super.devicePropertiesChanged(deviceInfo);
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
- getPercentType(deviceInfo.getBrightness()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
- getDecimalType(deviceInfo.getColorTemp()));
- publishState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
-
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
- getDecimalType(deviceInfo.getSignalLevel()));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
- getTimeType(deviceInfo.getOnTime(), Units.SECOND));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
- getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
- }
-
- /***********************************
- *
- * CHANNELS
- *
- ************************************/
- /**
- * Get ChannelID including group
- *
- * @param group String channel-group
- * @param channel String channel-name
- * @return String channelID
- */
- @Override
- protected String getChannelID(String group, String channel) {
- return group + "#" + channel;
- }
-
- /**
- * Get Channel from ChannelID
- *
- * @param channelID String channelID
- * @return String channel-name
- */
- @Override
- protected String getChannelFromID(ChannelUID channelID) {
- String channel = channelID.getIdWithoutGroup();
- channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
- channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
- channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
- return channel;
- }
-}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.bridge;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoBridgeConfiguration {
+ /* THING CONFIGUTATION PROPERTYS */
+ public static final String CONFIG_EMAIL = "username";
+ public static final String CONFIG_PASS = "password";
+ public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
+ public static final String CONFIG_DISCOVERY_UDP = "udpDiscovery";
+ public static final String CONFIG_DISCOVERY_ONLINE = "onlyLocalOnlineDevices";
+ public static final String CONFIG_BROADCAST_ADDRESS = "broadcastAddress";
+ public static final String CONFIG_DISCOVERY_INTERVAL = "discoveryInterval";
+
+ /* DEFAULT & FIXED CONFIGURATIONS */
+ public static final Integer CONFIG_CLOUD_FIXED_INTERVAL = 1440;
+
+ /* thing configuration parameter. */
+ public String username = "";
+ public String password = "";
+ public String broadcastAddress = "255.255.255.255";
+ public boolean cloudDiscovery = false;
+ public boolean udpDiscovery = false;
+ public boolean onlyLocalOnlineDevices = false;
+ public int reconnectInterval = CONFIG_CLOUD_FIXED_INTERVAL;
+ public int discoveryInterval = 60;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.bridge;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.util.Collection;
+import java.util.Set;
+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.api.TapoCloudConnector;
+import org.openhab.binding.tapocontrol.internal.discovery.TapoDiscoveryService;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.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;
+
+/**
+ * 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 TapoBridgeConfiguration config = new TapoBridgeConfiguration();
+ private final HttpClient httpClient;
+ private @Nullable ScheduledFuture<?> startupJob;
+ private @Nullable ScheduledFuture<?> pollingJob;
+ 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();
+ cloudConnector = new TapoCloudConnector(this);
+ credentials = new TapoCredentials();
+ uid = thing.getUID().toString();
+ this.httpClient = httpClient;
+ }
+
+ /***********************************
+ *
+ * BRIDGE INITIALIZATION
+ *
+ ************************************/
+ @Override
+ /**
+ * INIT BRIDGE
+ * set credentials and login cloud
+ */
+ public void initialize() {
+ config = getConfigAs(TapoBridgeConfiguration.class);
+ 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);
+ super.dispose();
+ }
+
+ /**
+ * ACTIVATE DISCOVERY SERVICE
+ */
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Set.of(TapoDiscoveryService.class);
+ }
+
+ /**
+ * Set DiscoveryService
+ *
+ * @param discoveryService
+ */
+ public void setDiscoveryService(TapoDiscoveryService discoveryService) {
+ this.discoveryService = discoveryService;
+ }
+
+ /***********************************
+ *
+ * SCHEDULER
+ *
+ ************************************/
+
+ /**
+ * delayed OneTime StartupJob
+ */
+ private void delayedStartUp() {
+ loginCloud();
+ startCloudScheduler();
+ discoveryService.startBackgroundDiscovery();
+ }
+
+ /**
+ * Start CloudLogin Scheduler
+ */
+ protected void startCloudScheduler() {
+ int pollingInterval = config.reconnectInterval;
+ TimeUnit timeUnit = TimeUnit.MINUTES;
+ if (pollingInterval > 0) {
+ logger.debug("{} starting cloudScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
+
+ this.pollingJob = scheduler.scheduleWithFixedDelay(this::loginCloud, pollingInterval, pollingInterval,
+ timeUnit);
+ } else {
+ logger.debug("({}) cloudScheduler disabled with config '0'", uid);
+ stopScheduler(this.pollingJob);
+ }
+ }
+
+ /**
+ * Stop scheduler
+ *
+ * @param scheduler ScheduledFeature which should be stopped
+ */
+ protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+ if (scheduler != null) {
+ scheduler.cancel(true);
+ scheduler = null;
+ }
+ }
+
+ /***********************************
+ *
+ * ERROR HANDLER
+ *
+ ************************************/
+
+ /**
+ * return device Error
+ *
+ * @return
+ */
+ public TapoErrorHandler getErrorHandler() {
+ return bridgeError;
+ }
+
+ /**
+ * set device error
+ *
+ * @param tapoError TapoErrorHandler-Object
+ */
+ public void setError(TapoErrorHandler tapoError) {
+ bridgeError.set(tapoError);
+ handleConnectionState();
+ }
+
+ /***********************************
+ *
+ * BRIDGE COMMUNICATIONS
+ *
+ ************************************/
+
+ /**
+ * Login to Cloud
+ *
+ * @return
+ */
+ public boolean loginCloud() {
+ bridgeError.reset(); // reset ErrorHandler
+ if (credentials.areSet()) {
+ try {
+ cloudConnector.login(credentials);
+ } catch (Exception e) {
+ logger.trace("({}) login to cloud failed", this.uid);
+ }
+ } else {
+ bridgeError.raiseError(ERR_BINDING_CREDENTIALS, "credentials not set");
+ }
+ handleConnectionState();
+ return cloudConnector.isLoggedIn();
+ }
+
+ /**
+ * Handle Connection state
+ */
+ private void handleConnectionState() {
+ if (cloudConnector.isLoggedIn() && !bridgeError.hasError()) {
+ updateStatus(ThingStatus.ONLINE);
+ } else if (bridgeError.hasError()) {
+ switch (bridgeError.getType()) {
+ case COMMUNICATION_ERROR:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, bridgeError.getMessage());
+ break;
+ case CONFIGURATION_ERROR:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, bridgeError.getMessage());
+ break;
+ default:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, bridgeError.getMessage());
+ }
+ } else {
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
+ }
+ }
+
+ /***********************************
+ *
+ * BRIDGE GETTERS
+ *
+ ************************************/
+
+ public TapoCredentials getCredentials() {
+ return credentials;
+ }
+
+ public HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+ public TapoCloudConnector getCloudConnector() {
+ return cloudConnector;
+ }
+
+ public TapoDiscoveryService getDiscoveryService() {
+ return discoveryService;
+ }
+
+ public ThingUID getUID() {
+ return getThing().getUID();
+ }
+
+ public TapoBridgeConfiguration getBridgeConfig() {
+ return config;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.bridge.dto;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+
+import java.util.UUID;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * TapoCloudLoginData Record for sending request
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoCloudLoginData(@Expose String appType, @Expose String cloudUserName, @Expose String cloudPassword,
+ @Expose String terminalUUID) {
+
+ public TapoCloudLoginData(String cloudUserName, String cloudPassword) {
+ this(TAPO_APP_TYPE, cloudUserName, cloudPassword, UUID.randomUUID().toString());
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.bridge.dto;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * TapoCloudLogin Result Class as record
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoCloudLoginResult(@Expose @SerializedName("accountId") String accountId,
+ @Expose @SerializedName("regTime") String regTime, @Expose @SerializedName("countryCode") String countryCode,
+ @Expose @SerializedName("riskDetected") int riskDetected, @Expose @SerializedName("nickname") String nickname,
+ @Expose @SerializedName("email") String email, @Expose @SerializedName("token") String token) {
+
+ /* init new emty record */
+ public TapoCloudLoginResult() {
+ this("", "", "", 0, "", "", "");
+ }
+
+ /**********************************************
+ * Return default data if recordobject is null
+ **********************************************/
+ @Override
+ public String accountId() {
+ return Objects.requireNonNullElse(accountId, "");
+ }
+
+ @Override
+ public String token() {
+ return Objects.requireNonNullElse(token, "");
+ }
+
+ @Override
+ public String email() {
+ return Objects.requireNonNullElse(email, "");
+ }
+
+ @Override
+ public String nickname() {
+ return Objects.requireNonNullElse(nickname, "");
+ }
+
+ @Override
+ public int riskDetected() {
+ return Objects.requireNonNullElse(riskDetected, 0);
+ }
+
+ @Override
+ public String countryCode() {
+ return Objects.requireNonNullElse(countryCode, "");
+ }
+
+ @Override
+ public String regTime() {
+ return Objects.requireNonNullElse(regTime, "");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.dto;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Base-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBaseDeviceData {
+ @SerializedName("device_id")
+ @Expose(serialize = false, deserialize = true)
+ private String deviceId = "";
+
+ @SerializedName("fw_ver")
+ @Expose(serialize = false, deserialize = true)
+ private String fwVer = "";
+
+ @SerializedName("hw_ver")
+ @Expose(serialize = false, deserialize = true)
+ private String hwVer = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String mac = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String model = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String nickname = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String region = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String type = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String lang = "";
+
+ @SerializedName("hw_id")
+ @Expose(serialize = false, deserialize = true)
+ private String hwId = "";
+
+ @SerializedName("fw_id")
+ @Expose(serialize = false, deserialize = true)
+ private String fwId = "";
+
+ @SerializedName("oem_id")
+ @Expose(serialize = false, deserialize = true)
+ private String oemId = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private String ip = "";
+
+ @SerializedName(value = "overheated", alternate = "overheatStatus")
+ @Expose(serialize = false, deserialize = true)
+ private boolean overheated = false;
+
+ @Expose(serialize = false, deserialize = true)
+ private int rssi = 0;
+
+ @SerializedName("signal_level")
+ @Expose(serialize = false, deserialize = true)
+ private int signalLevel = 0;
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public String getDeviceId() {
+ return deviceId;
+ }
+
+ public String getFirmwareVersion() {
+ return fwVer;
+ }
+
+ public String getFirmwareId() {
+ return fwId;
+ }
+
+ public String getHardwareVersion() {
+ return hwVer;
+ }
+
+ public String getHardwareId() {
+ return hwId;
+ }
+
+ public boolean isOverheated() {
+ return overheated;
+ }
+
+ public String getLanguage() {
+ return lang;
+ }
+
+ public String getMAC() {
+ return formatMac(mac, MAC_DIVISION_CHAR);
+ }
+
+ public String getModel() {
+ return model.replace(" Series", "");
+ }
+
+ public String getNickname() {
+ return nickname;
+ }
+
+ public String getOEM() {
+ return oemId;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public String getRepresentationProperty() {
+ return getMAC();
+ }
+
+ public int getSignalLevel() {
+ return signalLevel;
+ }
+
+ public int getRSSI() {
+ return rssi;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getIpAddress() {
+ return ip;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo Child Device Information class
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Christian Wild - Integrating TapoHub
+ */
+@NonNullByDefault
+public class TapoChildDeviceData extends TapoBaseDeviceData {
+ @SerializedName("at_low_battery")
+ @Expose(serialize = false, deserialize = true)
+ private boolean atLowBattery = false;
+
+ @SerializedName("battery_percentage")
+ @Expose(serialize = false, deserialize = true)
+ private int batteryPercentage = 0;
+
+ @SerializedName("bind_count")
+ @Expose(serialize = false, deserialize = true)
+ private int bindCount = 0;
+
+ @SerializedName("temp_unit")
+ @Expose(serialize = false, deserialize = true)
+ private String tempUnit = "celsius";
+
+ @SerializedName("current_temp")
+ @Expose(serialize = false, deserialize = true)
+ private double currentTemp = 0.0;
+
+ @SerializedName("current_humidity")
+ @Expose(serialize = false, deserialize = true)
+ private int currentHumidity = 0;
+
+ @Expose(serialize = false, deserialize = true)
+ private String category = "";
+
+ @SerializedName("device_on")
+ @Expose(serialize = true, deserialize = true)
+ private boolean deviceOn = false;
+
+ @SerializedName("jamming_rssi")
+ @Expose(serialize = false, deserialize = true)
+ private int jammingRssi = 0;
+
+ @SerializedName("jamming_signal_level")
+ @Expose(serialize = false, deserialize = true)
+ private int jammingSignalLevel = 0;
+
+ @SerializedName("lastOnboardingTimestamp")
+ @Expose(serialize = false, deserialize = true)
+ private long lastOnboardingTimestamp = 0;
+
+ @SerializedName("on_time")
+ @Expose(serialize = false, deserialize = true)
+ private long onTime = 0;
+
+ @Expose(serialize = false, deserialize = true)
+ private boolean open = false;
+
+ @SerializedName("parent_device_id")
+ @Expose(serialize = false, deserialize = true)
+ private String parentDeviceId = "";
+
+ @Expose(serialize = false, deserialize = true)
+ private int position = 0;
+
+ @SerializedName("report_interval")
+ @Expose(serialize = false, deserialize = true)
+ private int reportInterval = 0;
+
+ @SerializedName("slot_number")
+ @Expose(serialize = false, deserialize = true)
+ private int slotNumber = 0;
+
+ @Expose(serialize = false, deserialize = true)
+ private String status = "";
+
+ @SerializedName("status_follow_edge")
+ @Expose(serialize = false, deserialize = true)
+ private boolean statusFollowEedge = false;
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ /* get boolean values */
+ public boolean batteryIsLow() {
+ return atLowBattery;
+ }
+
+ public boolean getStatusFollowEedge() {
+ return statusFollowEedge;
+ }
+
+ public int getBatteryPercentage() {
+ return batteryPercentage;
+ }
+
+ public boolean isOff() {
+ return !deviceOn;
+ }
+
+ public boolean isOn() {
+ return deviceOn;
+ }
+
+ public double getTemperature() {
+ return currentTemp;
+ }
+
+ public String getTempUnit() {
+ return tempUnit;
+ }
+
+ public int getHumidity() {
+ return currentHumidity;
+ }
+
+ public boolean isOnline() {
+ return "online".equals(status);
+ }
+
+ public boolean isOpen() {
+ return open;
+ }
+
+ /* get numeric values */
+ public int getBindCount() {
+ return bindCount;
+ }
+
+ public int getJammingRssi() {
+ return jammingRssi;
+ }
+
+ public int getJammingSignalLevel() {
+ return jammingSignalLevel;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public int getReportInterval() {
+ return reportInterval;
+ }
+
+ public int getSlotNumber() {
+ return slotNumber;
+ }
+
+ public long getLastOnboardingTimestamp() {
+ return lastOnboardingTimestamp;
+ }
+
+ public Number getOnTime() {
+ return onTime;
+ }
+
+ /* get string values */
+ public String getCategory() {
+ return category;
+ }
+
+ public String getParentDeviceId() {
+ return parentDeviceId;
+ }
+
+ @Override
+ public String getRepresentationProperty() {
+ return getDeviceId();
+ }
+
+ /***********************************
+ *
+ * SET VALUES
+ *
+ ************************************/
+ public void setDeviceOn(boolean deviceOn) {
+ this.deviceOn = deviceOn;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.dto;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Child Structure Class
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class TapoChildList {
+ @Expose
+ @SerializedName("start_index")
+ private int startIndex = 0;
+
+ @Expose
+ private int sum = 0;
+
+ @Expose
+ @SerializedName("child_device_list")
+ private List<TapoChildDeviceData> childDeviceList = List.of();
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public int getStartIndex() {
+ return startIndex;
+ }
+
+ public int getSum() {
+ return sum;
+ }
+
+ public List<TapoChildDeviceData> getChildDeviceList() {
+ return childDeviceList;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.dto;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Energy-Monitor Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoEnergyData {
+ @SerializedName("local_time")
+ @Expose(serialize = false, deserialize = true)
+ private String localTime = "";
+
+ @SerializedName("current_power")
+ @Expose(serialize = false, deserialize = true)
+ private int currentPower = 0;
+
+ @SerializedName("electricity_charge")
+ @Expose(serialize = false, deserialize = true)
+ private List<Integer> electricityCharge = List.of();
+
+ @SerializedName("today_runtime")
+ @Expose(serialize = false, deserialize = true)
+ private int todayRuntime = 0;
+
+ @SerializedName("today_energy")
+ @Expose(serialize = false, deserialize = true)
+ private int todayEnergy = 0;
+
+ @SerializedName("month_energy")
+ @Expose(serialize = false, deserialize = true)
+ private int monthEnergy = 0;
+
+ @SerializedName("month_runtime")
+ @Expose(serialize = false, deserialize = true)
+ private int monthRuntime = 0;
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public ZonedDateTime getLocalDate() {
+ return ZonedDateTime.parse(localTime);
+ }
+
+ public double getCurrentPower() {
+ return (double) currentPower / 1000;
+ }
+
+ public List<Integer> getElectricityCharge() {
+ return electricityCharge;
+ }
+
+ public int getTodayEnergy() {
+ return todayEnergy;
+ }
+
+ public int getMonthEnergy() {
+ return monthEnergy;
+ }
+
+ public int getTodayRuntime() {
+ return todayRuntime;
+ }
+
+ public int getMonthRuntime() {
+ return monthRuntime;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Tapo-DynamicLightEffects Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightDynamicFx {
+ private static final String FX_JSON_FILE = "/lightningfx/dynamic_light_fx.json";
+
+ @Expose
+ private boolean enable = false;
+
+ @Expose
+ private String id = JSON_KEY_LIGHTNING_EFFECT_OFF;
+
+ @Expose(serialize = false, deserialize = true)
+ private String name = "";
+
+ /***********************************
+ *
+ * SET VALUES
+ *
+ ************************************/
+
+ public void setEffect(String id) throws TapoErrorHandler {
+ setEffect(getEffect(id));
+ }
+
+ public void setEffect(TapoLightDynamicFx effect) {
+ enable = effect.enable;
+ id = effect.id;
+ name = effect.name;
+ }
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public boolean isEnabled() {
+ return enable;
+ }
+
+ public String getId() {
+ if (!isEnabled()) {
+ return JSON_KEY_LIGHTNING_EFFECT_OFF;
+ }
+ return id;
+ }
+
+ public String getName() {
+ if (!isEnabled()) {
+ return JSON_KEY_LIGHTNING_EFFECT_OFF;
+ }
+ return name;
+ }
+
+ public boolean hasEffect(String id) {
+ try {
+ return getEffect(id) instanceof TapoLightDynamicFx;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /***********************************
+ *
+ * PRIVATE HELPERS
+ *
+ ************************************/
+
+ /**
+ * Load Dynamic Light FX-Data from JSON
+ *
+ * @param id id off effect
+ */
+ private TapoLightDynamicFx getEffect(String id) throws TapoErrorHandler {
+ List<TapoLightDynamicFx> effects = getEffectList();
+ for (TapoLightDynamicFx fx : effects) {
+ if (fx.id.equals(id)) {
+ return fx;
+ }
+ }
+ throw new TapoErrorHandler(TapoErrorCode.ERR_BINDING_FX_NOT_FOUND);
+ }
+
+ /**
+ * Load All Effects as List from JSON
+ */
+ private List<TapoLightDynamicFx> getEffectList() throws TapoErrorHandler {
+ InputStream is = getClass().getResourceAsStream(FX_JSON_FILE);
+ if (is != null) {
+ try {
+ Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
+ Type listType = new TypeToken<ArrayList<TapoLightDynamicFx>>() {
+ }.getType();
+ return GSON.fromJson(reader, listType);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(TapoErrorCode.ERR_API_JSON_DECODE_FAIL);
+ }
+ } else {
+ throw new TapoErrorHandler(TapoErrorCode.ERR_BINDING_FX_NOT_FOUND);
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-LightEffects Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightEffect {
+ @Expose
+ private String id = "";
+
+ @Expose
+ private String name = "";
+
+ @Expose
+ private Integer brightness = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("display_colors")
+ private List<int[]> displayColors = List.of();
+
+ @Expose
+ private Integer enable = 0;
+
+ @Expose
+ @Nullable
+ private Integer bAdjusted = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("brightness_range")
+ private Integer[] brightnessRange = {};
+
+ @Expose
+ @Nullable
+ private List<int[]> backgrounds = List.of();
+
+ @Expose
+ @Nullable
+ private Integer custom = 0;
+
+ @Expose
+ @Nullable
+ private Integer direction = 0;
+
+ @Expose
+ @Nullable
+ private Integer duration = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("expansion_strategy")
+ private Integer expansionStrategy = 0;
+
+ @Expose
+ @Nullable
+ private Integer fadeoff = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("hue_range")
+ private Integer[] hueRange = {};
+
+ @SerializedName("init_states")
+ @Expose
+ @Nullable
+ private List<Integer[]> initStates = List.of();
+ @Expose
+ @Nullable
+ @SerializedName("random_seed")
+ private Integer randomSeed = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("repeat_times")
+ private Integer repeatTimes = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("saturation_range")
+ private Integer[] saturationRange = {};
+
+ @Expose
+ @Nullable
+ @SerializedName("segment_length")
+ private Integer segmentLength = 0;
+
+ @Expose
+ @Nullable
+ private Integer[] segments = {};
+
+ @Expose
+ @Nullable
+ private List<int[]> sequence = List.of();
+
+ @Expose
+ @Nullable
+ private Integer spread = 0;
+
+ @Expose
+ @Nullable
+ private Integer transition = 0;
+
+ @Expose
+ @Nullable
+ @SerializedName("transition_range")
+ private Integer[] transitionRange = {};
+
+ @Expose
+ @Nullable
+ private String type = "";
+
+ @Expose
+ @Nullable
+ @SerializedName("trans_sequence")
+ private List<int[]> transSequence = List.of();
+
+ @Expose
+ @Nullable
+ @SerializedName("run_time")
+ private Integer runTime = 0;
+
+ /**
+ * Init class with effect id
+ */
+ public TapoLightEffect(boolean enable, String fxId) {
+ setEnable(enable);
+ id = fxId;
+ }
+
+ public TapoLightEffect(String fxId) {
+ setEnable((fxId.length() > 0 && !fxId.equals(JSON_KEY_LIGHTNING_EFFECT_OFF)));
+ id = fxId;
+ }
+
+ public TapoLightEffect() {
+ }
+
+ /***********************************
+ *
+ * SET VALUES
+ *
+ ************************************/
+
+ public void setEnable(boolean enable) {
+ this.enable = (enable) ? 1 : 0;
+ }
+
+ public void setBrightness(Integer value) {
+ brightness = value;
+ }
+
+ /**
+ * set light fx from fx-name
+ * loads fx data from resources/lightningfx/[fxname].json
+ *
+ * @param fxName name of effect
+ * @return
+ */
+ public TapoLightEffect setEffect(String fxName) throws TapoErrorHandler {
+ if (JSON_KEY_LIGHTNING_EFFECT_OFF.equals(fxName)) {
+ enable = 0;
+ return this;
+ } else {
+ InputStream is = getClass().getResourceAsStream("/lightningfx/" + fxName + ".json");
+ if (is != null) {
+ try {
+ Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
+ return GSON.fromJson(reader, TapoLightEffect.class);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(TapoErrorCode.ERR_API_JSON_DECODE_FAIL, fxName);
+ }
+ } else {
+ throw new TapoErrorHandler(TapoErrorCode.ERR_BINDING_FX_NOT_FOUND, fxName);
+ }
+ }
+ }
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public boolean isEnabled() {
+ return enable == 1;
+ }
+
+ public String getName() {
+ if (!isEnabled()) {
+ return JSON_KEY_LIGHTNING_EFFECT_OFF;
+ }
+ return name;
+ }
+
+ public Integer getBrightness() {
+ return brightness;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.rf;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.hub.TapoHubHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Basic Child-Device-Handler
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public abstract class TapoChildDeviceHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoChildDeviceHandler.class);
+ protected final TapoErrorHandler deviceError = new TapoErrorHandler();
+ protected final String uid;
+ protected final String deviceId;
+ protected @NonNullByDefault({}) TapoHubHandler hub;
+ private TapoChildDeviceData deviceInfo = new TapoChildDeviceData();
+ private Map<String, Object> oldStates = new HashMap<>();
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ protected TapoChildDeviceHandler(Thing thing) {
+ super(thing);
+ uid = getThing().getUID().getAsString();
+ deviceId = getValueOrDefault(getThing().getProperties().get(CHILD_REPRESENTATION_PROPERTY), "");
+ }
+
+ /***********************************
+ *
+ * INIT AND SETTINGS
+ *
+ ************************************/
+
+ /**
+ * INITIALIZE DEVICE
+ */
+ @Override
+ public void initialize() {
+ logger.trace("({}) Initializing thing ", uid);
+ Bridge bridgeThing = getBridge();
+ if (bridgeThing != null) {
+ if (bridgeThing.getHandler() instanceof TapoHubHandler tapoHubHandler) {
+ this.hub = tapoHubHandler;
+ }
+ activateDevice();
+ } else {
+ deviceError.raiseError(ERR_CONFIG_NO_BRIDGE);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
+ }
+ }
+
+ /**
+ * ACTIVATE DEVICE
+ */
+ private void activateDevice() {
+ // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+ updateStatus(ThingStatus.UNKNOWN);
+
+ if (hub.getChild(deviceId).isOnline()) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE);
+ }
+ }
+
+ /**
+ * handle command sent to device
+ *
+ * @param channelUID channelUID command is sent to
+ * @param command command to be sent
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ /* perform actions */
+ if (command instanceof RefreshType) {
+ setDeviceData();
+ } else {
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+ }
+ }
+
+ /***********************************
+ *
+ * DEVICE PROPERTIES
+ *
+ ************************************/
+
+ /**
+ * refresh child properties and data from hub data
+ */
+ public void setDeviceData() {
+ setDeviceData(hub.getChild(deviceId));
+ }
+
+ /*
+ * refresh child properties and data from hub data
+ */
+ public void setDeviceData(TapoChildDeviceData deviceInfo) {
+ this.deviceInfo = deviceInfo;
+ triggerEvents(deviceInfo);
+ devicePropertiesChanged(deviceInfo);
+ handleConnectionState();
+ }
+
+ /**
+ * handle device state by connector error
+ */
+ private void handleConnectionState() {
+ if (deviceInfo.isOnline()) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+ }
+ }
+
+ /**
+ * UPDATE PROPERTIES
+ *
+ * @param deviceInfo ChildDeviceData
+ */
+ protected void devicePropertiesChanged(TapoChildDeviceData deviceInfo) {
+ logger.trace("({}) devicePropertiesChanged ", uid);
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_SIGNAL_STRENGTH),
+ getDecimalType(deviceInfo.getSignalLevel()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_BATTERY_LOW), getOnOffType(deviceInfo.batteryIsLow()));
+ }
+
+ /**
+ * Fires events on {@link TapoChildDeviceData} changes.
+ */
+ protected void triggerEvents(TapoChildDeviceData deviceInfo) {
+ if (checkForStateChange(CHANNEL_BATTERY_LOW, deviceInfo.batteryIsLow())) {
+ if (deviceInfo.batteryIsLow()) {
+ triggerChannel(getChannelID(CHANNEL_GROUP_DEVICE, EVENT_BATTERY_LOW), EVENT_STATE_BATTERY_LOW);
+ }
+ }
+ }
+
+ /***********************************
+ *
+ * CHANNELS
+ *
+ ************************************/
+ /**
+ * Get ChannelID including group
+ *
+ * @param group String channel-group
+ * @param channel String channel-name
+ * @return String channelID
+ */
+ protected String getChannelID(String group, String channel) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (CHANNEL_GROUP_THING_SET.contains(thingTypeUID) && group.length() > 0) {
+ return group + "#" + channel;
+ }
+ return channel;
+ }
+
+ /**
+ * Checks if the state changed since the last channel update.
+ *
+ * @param stateName the name of the state (channel)
+ * @param comparator comparison value
+ * @return <code>true</code> if changed, <code>false</code> if not or no old value exists
+ */
+ protected boolean checkForStateChange(String stateName, Object comparator) {
+ if (oldStates.get(stateName) == null) {
+ oldStates.put(stateName, comparator);
+ } else if (!comparator.equals(oldStates.get(stateName))) {
+ oldStates.put(stateName, comparator);
+ return true;
+ }
+ return false;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.rf.smartcontact;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.rf.TapoChildDeviceHandler;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Contact-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSmartContactHandler extends TapoChildDeviceHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoSmartContactHandler.class);
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoSmartContactHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Update properties
+ */
+ @Override
+ protected void devicePropertiesChanged(TapoChildDeviceData deviceInfo) {
+ super.devicePropertiesChanged(deviceInfo);
+ updateState(getChannelID(CHANNEL_GROUP_SENSOR, CHANNEL_IS_OPEN), getOnOffType(deviceInfo.isOpen()));
+ }
+
+ /**
+ * Fires events on {@link TapoChildDeviceData} changes.
+ */
+ @Override
+ protected void triggerEvents(TapoChildDeviceData deviceInfo) {
+ super.triggerEvents(deviceInfo);
+ if (checkForStateChange(CHANNEL_IS_OPEN, deviceInfo.isOpen())) {
+ if (deviceInfo.isOpen()) {
+ triggerChannel(getChannelID(CHANNEL_GROUP_SENSOR, EVENT_CONTACT_OPENED), EVENT_STATE_OPENED);
+ logger.trace("({}) contact event fired '{}'", uid, EVENT_STATE_OPENED);
+ } else {
+ triggerChannel(getChannelID(CHANNEL_GROUP_SENSOR, EVENT_CONTACT_CLOSED), EVENT_STATE_CLOSED);
+ logger.trace("({}) contact event fired '{}'", uid, EVENT_STATE_CLOSED);
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.rf.wheatersensor;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.rf.TapoChildDeviceHandler;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.Thing;
+
+/**
+ * TAPO Smart-Contact-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoWheaterSensorHandler extends TapoChildDeviceHandler {
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoWheaterSensorHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Update properties
+ */
+ @Override
+ protected void devicePropertiesChanged(TapoChildDeviceData deviceInfo) {
+ super.devicePropertiesChanged(deviceInfo);
+
+ updateState(getChannelID(CHANNEL_GROUP_SENSOR, CHANNEL_TEMPERATURE),
+ getTemperatureType(deviceInfo.getTemperature(), SIUnits.CELSIUS));
+ updateState(getChannelID(CHANNEL_GROUP_SENSOR, CHANNEL_HUMIDITY), getPercentType(deviceInfo.getHumidity()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoEnergyData;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract class as base for TAPO-Device device implementations.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public abstract class TapoBaseDeviceHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoBaseDeviceHandler.class);
+ protected final TapoErrorHandler deviceError = new TapoErrorHandler();
+ protected final String uid;
+ protected TapoDeviceConfiguration deviceConfig = new TapoDeviceConfiguration();
+ protected TapoBaseDeviceData baseDeviceData = new TapoBaseDeviceData();
+ protected TapoEnergyData energyData = new TapoEnergyData();
+ protected @Nullable ScheduledFuture<?> startupJob;
+ protected @Nullable ScheduledFuture<?> pollingJob;
+ protected @NonNullByDefault({}) TapoDeviceConnector connector;
+ protected @NonNullByDefault({}) TapoBridgeHandler bridge;
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ protected TapoBaseDeviceHandler(Thing thing) {
+ super(thing);
+ this.uid = getThing().getUID().getAsString();
+ }
+
+ /***********************************
+ * INIT AND SETTINGS
+ ************************************/
+
+ /**
+ * INITIALIZE DEVICE
+ */
+ @Override
+ public void initialize() {
+ try {
+ deviceConfig = getConfigAs(TapoDeviceConfiguration.class);
+ initConnector();
+ if (checkRequirements()) {
+ activateDevice();
+ }
+ } catch (TapoErrorHandler te) {
+ logger.warn("({}) deviceConfiguration error : {}", uid, te.getMessage());
+ setError(te);
+ } catch (Exception e) {
+ logger.debug("({}) error initializing device : {}", uid, e.getMessage());
+ }
+ }
+
+ /**
+ * Init TapoBridgeHandler TapoDeviceConnector
+ */
+ protected void initConnector() {
+ Bridge bridgeThing = getBridge();
+ if (bridgeThing != null) {
+ BridgeHandler bridgeHandler = bridgeThing.getHandler();
+ if (bridgeHandler instanceof TapoBridgeHandler tapoBridgeHandler) {
+ this.bridge = tapoBridgeHandler;
+ this.connector = new TapoDeviceConnector(this, bridge);
+ }
+ }
+ }
+
+ /**
+ * DISPOSE
+ */
+ @Override
+ public void dispose() {
+ try {
+ stopScheduler(this.startupJob);
+ stopScheduler(this.pollingJob);
+ connector.logout();
+ } catch (Exception e) {
+ // handle exception
+ }
+ super.dispose();
+ }
+
+ /**
+ * ACTIVATE DEVICE
+ */
+ protected 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);
+ }
+
+ /**
+ * Check if bridge is set
+ */
+ protected boolean checkBridge() throws TapoErrorHandler {
+ /* check bridge */
+ if (!(bridge instanceof TapoBridgeHandler)) {
+ throw new TapoErrorHandler(ERR_CONFIG_NO_BRIDGE);
+ }
+ /* check credentials */
+ if (!bridge.getCredentials().areSet()) {
+ throw new TapoErrorHandler(ERR_CONFIG_CREDENTIALS);
+ }
+ return true;
+ }
+
+ /**
+ * Check if Bridge is set and deviceConfiguration is set
+ */
+ protected boolean checkRequirements() throws TapoErrorHandler {
+ return (checkBridge() && deviceConfig.checkConfig());
+ }
+
+ /**
+ * 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();
+ startPollingScheduler();
+ }
+
+ /**
+ * Start scheduler
+ */
+ protected void startPollingScheduler() {
+ int pollingInterval = deviceConfig.pollingInterval;
+ TimeUnit timeUnit = TimeUnit.SECONDS;
+
+ if (pollingInterval > 0) {
+ if (pollingInterval < POLLING_MIN_INTERVAL_S) {
+ pollingInterval = POLLING_MIN_INTERVAL_S;
+ }
+ logger.debug("({}) startScheduler: create job with interval : {} {}", uid, pollingInterval, timeUnit);
+ this.pollingJob = scheduler.scheduleWithFixedDelay(this::pollingSchedulerAction, pollingInterval,
+ pollingInterval, timeUnit);
+ } else {
+ logger.debug("({}) scheduler disabled with deviceConfig '0'", uid);
+ stopScheduler(this.pollingJob);
+ }
+ }
+
+ /**
+ * Stop scheduler
+ *
+ * @param scheduler ScheduledFeature which should be stopped
+ */
+ protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+ if (scheduler != null) {
+ scheduler.cancel(true);
+ }
+ }
+
+ /**
+ * Scheduler Action
+ */
+ protected void pollingSchedulerAction() {
+ logger.trace("({}) schedulerAction", uid);
+ queryDeviceData();
+ }
+
+ /***********************************
+ * ERROR HANDLER
+ ************************************/
+ /**
+ * return device Error
+ */
+ public TapoErrorHandler getErrorHandler() {
+ return deviceError;
+ }
+
+ public TapoErrorCode getError() {
+ return deviceError.getError();
+ }
+
+ /**
+ * set device error
+ *
+ * @param tapoError TapoErrorHandler-Object
+ */
+ public void setError(TapoErrorHandler tapoError) {
+ deviceError.set(tapoError);
+ handleConnectionState();
+ }
+
+ /***********************************
+ * THING
+ ************************************/
+
+ /***
+ * Check if ThingType is model
+ *
+ * @param model
+ * @return
+ */
+ protected Boolean isThingModel(String model) {
+ try {
+ model = getDeviceModel(model);
+ 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 baseDeviceData basebaseDeviceData
+ * @return true if is the expected device
+ */
+ protected boolean isExpectedThing(TapoBaseDeviceData baseDeviceData) {
+ try {
+ String expectedThingUID = getThing().getProperties().get(DEVICE_REPRESENTATION_PROPERTY);
+ String foundThingUID = baseDeviceData.getRepresentationProperty();
+ String foundModel = baseDeviceData.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();
+ }
+
+ /**
+ * Return ipAdress
+ */
+ public String getIpAddress() {
+ return deviceConfig.ipAddress;
+ }
+
+ /*
+ * return device configuration
+ */
+ public TapoDeviceConfiguration getDeviceConfig() {
+ return deviceConfig;
+ }
+
+ /***********************************
+ * DEVICE PROPERTIES
+ ************************************/
+
+ /**
+ * query default device properties
+ * query baseDeviceData, energyData (if available for device) and childData (if available for device).
+ */
+ public void queryDeviceData() {
+ queryDeviceData(false);
+ }
+
+ /**
+ * query default device properties
+ * query baseDeviceData, energyData (if available for device)
+ *
+ * @param ignoreGap ignore gap to last query. query anyway (force)
+ */
+ public void queryDeviceData(boolean ignoreGap) {
+ deviceError.reset();
+ if (isLoggedIn(LOGIN_RETRIES)) {
+ if (SUPPORTED_ENERGY_DATA_UIDS.contains(getThing().getThingTypeUID())) {
+ List<TapoRequest> requests = new ArrayList<>();
+ requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+ requests.add(new TapoRequest(DEVICE_CMD_GETENERGY));
+ connector.sendMultipleRequest(requests, ignoreGap);
+ } else {
+ connector.sendQueryCommand(DEVICE_CMD_GETINFO, ignoreGap);
+ }
+ }
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ public void newDataResult(String queryCommand) {
+ switch (queryCommand) {
+ case DEVICE_CMD_GETINFO:
+ baseDeviceData = connector.getResponseData(TapoBaseDeviceData.class);
+ updateBaseDeviceData();
+ break;
+ case DEVICE_CMD_GETENERGY:
+ energyData = connector.getResponseData(TapoEnergyData.class);
+ updateEnergyData(energyData);
+ break;
+ case DEVICE_CMD_CUSTOM:
+ responsePasstrough(connector.getResponseData(TapoResponse.class));
+ break;
+ default:
+ responsePasstrough(connector.getResponseData(TapoResponse.class));
+ break;
+ }
+ handleConnectionState();
+ }
+
+ /**
+ * handle baseDeviceData
+ */
+ private void updateBaseDeviceData() {
+ if (!baseDeviceData.getDeviceId().isBlank()) {
+ if (isExpectedThing(baseDeviceData)) {
+ updateDeviceProperties(baseDeviceData);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "found type:'" + baseDeviceData.getModel() + "' with mac:'"
+ + baseDeviceData.getRepresentationProperty() + "'. Check IP-Address");
+ }
+ } else {
+ logger.debug("({}) tried to update device with empty data", uid);
+ }
+ }
+
+ /**
+ * Handle full responsebody received from connector
+ *
+ * @param fullResponse complete TapoResponse
+ */
+ public void responsePasstrough(TapoResponse fullResponse) {
+ }
+
+ /***********************************
+ *
+ * CONNECTION
+ *
+ ************************************/
+
+ /**
+ * Connect (login) to device
+ *
+ */
+ public boolean connect() {
+ deviceError.reset();
+ boolean loginSuccess = false;
+
+ try {
+ loginSuccess = connector.login();
+ if (loginSuccess) {
+ queryDeviceData(true);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
+ }
+ } catch (Exception e) {
+ updateStatus(ThingStatus.UNKNOWN);
+ }
+ return loginSuccess;
+ }
+
+ /**
+ * disconnect device
+ */
+ public void disconnect() {
+ connector.logout();
+ }
+
+ /**
+ * check if is device is loged in.
+ *
+ * @param totalRetries times to retry login
+ */
+ public boolean isLoggedIn(int totalRetries) {
+ if (connector.isLoggedIn()) {
+ return true;
+ } else {
+ logger.debug("({}) check if logged in but is not", uid);
+ for (int count = 0; count < totalRetries; count++) {
+ connect();
+ }
+ return false;
+ }
+ }
+
+ /**
+ * handle device state by connector error
+ */
+ public void handleConnectionState() {
+ ThingStatus deviceState = getThing().getStatus();
+ TapoErrorCode errorCode = deviceError.getError();
+
+ if (errorCode == TapoErrorCode.NO_ERROR) {
+ if (deviceState != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } else {
+ switch (errorCode.getType()) {
+ case COMMUNICATION_RETRY:
+ connect();
+ break;
+ case COMMUNICATION_ERROR:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, deviceError.getMessage());
+ disconnect();
+ break;
+ case CONFIGURATION_ERROR:
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, deviceError.getMessage());
+ break;
+ default:
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, deviceError.getMessage());
+ }
+ }
+ }
+
+ /***********************************
+ * CHANNELS
+ ************************************/
+
+ /**
+ * Update Basic Device Properties
+ *
+ * If only one property must be changed, there is also a convenient method
+ * updateProperty(String name, String value).
+ *
+ * @param baseDeviceData Object BaseDeviceData
+ */
+ protected void updateDeviceProperties(TapoBaseDeviceData baseDeviceData) {
+ /* device properties */
+ Map<String, String> properties = editProperties();
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, baseDeviceData.getMAC());
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION, baseDeviceData.getFirmwareVersion());
+ properties.put(Thing.PROPERTY_HARDWARE_VERSION, baseDeviceData.getHardwareVersion());
+ properties.put(Thing.PROPERTY_MODEL_ID, baseDeviceData.getModel());
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, baseDeviceData.getDeviceId());
+ updateProperties(properties);
+ }
+
+ /**
+ * Update Energy Data Channels
+ */
+ protected void updateEnergyData(TapoEnergyData energyData) {
+ updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_POWER),
+ getPowerType(energyData.getCurrentPower(), Units.WATT));
+ updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_USAGE_TODAY),
+ getEnergyType(energyData.getTodayEnergy(), Units.WATT_HOUR));
+ updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_RUNTIME_TODAY),
+ getTimeType(energyData.getTodayRuntime(), Units.MINUTE));
+ updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_USAGE_MONTH),
+ getEnergyType(energyData.getMonthEnergy(), Units.WATT_HOUR));
+ updateState(getChannelID(CHANNEL_GROUP_ENERGY, CHANNEL_NRG_RUNTIME_MONTH),
+ getTimeType(energyData.getMonthRuntime(), Units.MINUTE));
+ }
+
+ /**
+ * Update custom channels of device
+ *
+ * @param baseDeviceData extended devicebelonging dataclass
+ */
+ protected void updateChannels(final Class<? extends TapoBaseDeviceData> baseDeviceData) {
+ }
+
+ /**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolEnum;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+
+@NonNullByDefault
+public final class TapoDeviceConfiguration {
+ /* THING CONFIGUTATION PROPERTYS */
+ public static final String CONFIG_DEVICE_IP = "ipAddress";
+ public static final String CONFIG_PROTOCOL = "protocol";
+ public static final String CONFIG_HTTP_PORT = "httpPort";
+ public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
+ public static final String CONFIG_BACKGROUND_DISCOVERY = "backgroundDiscovery";
+
+ /* thing configuration parameter. */
+ public String ipAddress = "";
+ public String protocol = "AES";
+ public int httpPort = 80;
+ public int pollingInterval = 30;
+ public boolean backgroundDiscovery = false;
+
+ /**
+ * Check for configuration errors
+ *
+ * @return true if config is valid
+ * @throws TapoErrorHandler
+ */
+ public boolean checkConfig() throws TapoErrorHandler {
+ if (!ipAddress.matches(IPV4_REGEX)) {
+ throw new TapoErrorHandler(ERR_CONFIG_IP);
+ }
+ try {
+ TapoProtocolEnum.valueOfString(protocol);
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_CONFIG_PROTOCOL);
+ }
+ return true;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.lightstrip.TapoLightStripData;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Universal-Device
+ * universal device for testing pruposes
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoUniversalDeviceHandler extends TapoBaseDeviceHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoUniversalDeviceHandler.class);
+ private TapoLightStripData deviceData = new TapoLightStripData();
+ private TapoRequest manualRequest = new TapoRequest(DEVICE_CMD_GETINFO);
+ private boolean useSecurePassthrough = true;
+
+ // Channel List for "Test- and Debug-Device"
+ public static final String CHANNEL_GROUP_RESPONSE = "response";
+ public static final String CHANNEL_GROUP_COMMAND = "devicecommand";
+ public static final String CHANNEL_RESPONSE = "deviceResponse";
+ public static final String CHANNEL_COMMAND_METHOD = "method";
+ public static final String CHANNEL_COMMAND_PARAMS = "params";
+ public static final String CHANNEL_COMMAND_SECURE = "secure";
+ public static final String CHANNEL_COMMAND_SEND = "sendCommand";
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoUniversalDeviceHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ @Override
+ public void newDataResult(String queryCommand) {
+ super.newDataResult(queryCommand);
+ if (DEVICE_CMD_GETINFO.equals(queryCommand)) {
+ deviceData = connector.getResponseData(TapoLightStripData.class);
+ updateChannels(deviceData);
+ }
+ }
+
+ /**
+ * query device Properties
+ */
+ @Override
+ public void queryDeviceData() {
+ deviceError.reset();
+ if (isLoggedIn(LOGIN_RETRIES)) {
+ connector.sendQueryCommand(DEVICE_CMD_GETINFO, true);
+ connector.sendQueryCommand(DEVICE_CMD_GETENERGY, true);
+ }
+ }
+
+ /*****************************
+ * HANDLE COMMANDS
+ *****************************/
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command, channelUID.getId());
+
+ String group = channelUID.getGroupId();
+ if (command instanceof RefreshType) {
+ queryDeviceData();
+ } else {
+ if (CHANNEL_GROUP_RESPONSE.equals(group) || CHANNEL_GROUP_COMMAND.equals(group)) {
+ handleSpecialCommands(channelUID, command);
+ } else {
+ handleStandardCommands(channelUID, command);
+ }
+ }
+ }
+
+ /**
+ * Handle standard commands for debug-, test-devices
+ */
+ private void handleStandardCommands(ChannelUID channelUID, Command command) {
+ String channel = channelUID.getIdWithoutGroup();
+
+ switch (channel) {
+ case CHANNEL_OUTPUT:
+ handleOnOffCommand(command);
+ break;
+ case CHANNEL_BRIGHTNESS:
+ handleBrightnessCommand(command);
+ break;
+ case CHANNEL_COLOR_TEMP:
+ handleColorTempCommand(command);
+ break;
+ case CHANNEL_COLOR:
+ handleColorCommand(command);
+ break;
+ default:
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+ }
+ }
+
+ /**
+ * Handle special commands for debug-, test-devices
+ */
+ private void handleSpecialCommands(ChannelUID channelUID, Command command) {
+ String channel = channelUID.getIdWithoutGroup();
+
+ if (CHANNEL_GROUP_COMMAND.equals(channelUID.getGroupId())) {
+ switch (channel) {
+ case CHANNEL_COMMAND_METHOD:
+ manualRequest = new TapoRequest(command.toString(), manualRequest.params());
+ break;
+ case CHANNEL_COMMAND_PARAMS:
+ manualRequest = new TapoRequest(manualRequest.method(), command.toString());
+ break;
+ case CHANNEL_COMMAND_SECURE:
+ useSecurePassthrough = !useSecurePassthrough;
+ break;
+ case CHANNEL_COMMAND_SEND:
+ /* send manual request */
+ if (useSecurePassthrough) {
+ logger.debug("({}) sendSecurePasstrough '{}' ", uid, manualRequest);
+ connector.sendAsyncRequest(manualRequest);
+ } else {
+ logger.debug("({}) sendRawCommand '{}' ", uid, manualRequest);
+ connector.sendRawCommand(manualRequest);
+ }
+ break;
+ default:
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command,
+ channelUID.getId());
+ }
+
+ } else {
+ if (CHANNEL_RESPONSE.equals(channel)) {
+ logger.debug("({}) NOT IMPLEMENTED COMMAND {} ", uid, command);
+ }
+ }
+ }
+
+ private void handleOnOffCommand(Command command) {
+ switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+ }
+
+ private void handleBrightnessCommand(Command command) {
+ if (command instanceof PercentType percentCommand) {
+ Float percent = percentCommand.floatValue();
+ setBrightness(percent.intValue()); // 0..100% = 0..100
+ } else if (command instanceof DecimalType decimalCommand) {
+ setBrightness(decimalCommand.intValue());
+ }
+ }
+
+ private void handleColorCommand(Command command) {
+ if (command instanceof HSBType hsbCommand) {
+ setColor(hsbCommand);
+ }
+ }
+
+ private void handleColorTempCommand(Command command) {
+ if (command instanceof DecimalType decimalCommand) {
+ setColorTemp(decimalCommand.intValue());
+ }
+ }
+
+ /*****************************
+ * SEND COMMANDS
+ *****************************/
+
+ /**
+ * Switch device On or Off
+ *
+ * @param on if true device will switch on. Otherwise switch off
+ */
+ protected void switchOnOff(boolean on) {
+ deviceData.switchOnOff(on);
+ connector.sendCommandAndQuery(deviceData, false);
+ }
+
+ /**
+ * SET BRIGHTNESS
+ *
+ * @param newBrightness percentage 0-100 of new brightness
+ */
+ protected void setBrightness(Integer newBrightness) {
+ /* switch off if 0 */
+ if (newBrightness == 0) {
+ deviceData.switchOff();
+ } else {
+ deviceData.switchOn();
+ deviceData.setBrightness(newBrightness);
+ connector.sendCommandAndQuery(deviceData, false);
+ }
+ }
+
+ /**
+ * SET COLOR
+ *
+ * @param command HSBType
+ */
+ protected void setColor(HSBType command) {
+ deviceData.switchOn();
+ deviceData.setHue(command.getHue().intValue());
+ deviceData.setSaturation(command.getSaturation().intValue());
+ deviceData.setBrightness(command.getBrightness().intValue());
+ connector.sendCommandAndQuery(deviceData, false);
+ }
+
+ /**
+ * SET COLORTEMP
+ *
+ * @param colorTemp (Integer) in Kelvin
+ */
+ protected void setColorTemp(Integer colorTemp) {
+ deviceData.switchOn();
+ deviceData.setColorTemp(colorTemp);
+ connector.sendCommandAndQuery(deviceData, false);
+ }
+
+ /**
+ * Handle full responsebody received from connector
+ *
+ * @param fullResponse TapoResponse received
+ */
+ @Override
+ public void responsePasstrough(TapoResponse fullResponse) {
+ String response = fullResponse.result().getAsString();
+ logger.debug("({}) received response {}", uid, response);
+ updateState(getChannelID(CHANNEL_GROUP_RESPONSE, CHANNEL_RESPONSE), getStringType(response));
+ }
+
+ /**
+ * UPDATE PROPERTIES
+ *
+ * @param deviceInfo TapoLightStripData
+ */
+ protected void updateChannels(TapoLightStripData deviceInfo) {
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceInfo.isOn()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+ getPercentType(deviceInfo.getBrightness()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+ getDecimalType(deviceInfo.getColorTemp()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceInfo.getHSB());
+
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+ getDecimalType(deviceInfo.getSignalLevel()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+ getTimeType(deviceInfo.getOnTime(), Units.SECOND));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
+ getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.bulb;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBulbData extends TapoBaseDeviceData {
+ @SerializedName("device_on")
+ @Expose(serialize = true, deserialize = true)
+ private boolean deviceOn = false;
+
+ @Expose(serialize = true, deserialize = true)
+ private int brightness = 100;
+
+ @SerializedName("color_temp")
+ @Expose(serialize = true, deserialize = true)
+ private int colorTemp = 0;
+
+ @Expose(serialize = true, deserialize = true)
+ private int hue = 0;
+
+ @Expose(serialize = true, deserialize = true)
+ private int saturation = 100;
+
+ @Expose(serialize = false, deserialize = true)
+ private long onTime = 0;
+
+ @SerializedName("time_usage_past7")
+ @Expose(serialize = false, deserialize = true)
+ private long timeUsagePast7 = 0;
+
+ @SerializedName("time_usage_past30")
+ @Expose(serialize = false, deserialize = true)
+ private long timeUsagePast30 = 0;
+
+ @SerializedName("time_usage_today")
+ @Expose(serialize = false, deserialize = true)
+ private long timeUsageToday = 0;
+
+ @SerializedName("dynamic_light_effect_enable")
+ @Expose(serialize = false, deserialize = true)
+ private boolean dynamicLightEffectEnable = false;
+
+ @SerializedName("dynamic_light_effect_id")
+ @Expose(serialize = false, deserialize = true)
+ private String dynamicLightEffectId = "";
+
+ /***********************************
+ *
+ * SET VALUES
+ *
+ ************************************/
+
+ public void switchOn() {
+ deviceOn = true;
+ }
+
+ public void switchOff() {
+ deviceOn = false;
+ }
+
+ public void switchOnOff(boolean on) {
+ deviceOn = on;
+ }
+
+ public void setBrightness(int value) {
+ brightness = value;
+ }
+
+ public void setColorTemp(int value) {
+ colorTemp = value;
+ }
+
+ public void setHue(int value) {
+ hue = value;
+ }
+
+ public void setSaturation(int value) {
+ saturation = value;
+ }
+
+ public void setDynamicLightEffectId(String fxId) {
+ dynamicLightEffectId = fxId;
+ }
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+ public boolean dynamicLightEffectEnabled() {
+ return dynamicLightEffectEnable;
+ }
+
+ public String getDynamicLightEffectId() {
+ return dynamicLightEffectId;
+ }
+
+ public int getBrightness() {
+ return brightness;
+ }
+
+ public int getColorTemp() {
+ return colorTemp;
+ }
+
+ public HSBType getHSB() {
+ DecimalType h = new DecimalType(hue);
+ PercentType s = new PercentType(saturation);
+ PercentType b = new PercentType(brightness);
+ return new HSBType(h, s, b);
+ }
+
+ public int getHue() {
+ return hue;
+ }
+
+ public boolean isOff() {
+ return !deviceOn;
+ }
+
+ public boolean isOn() {
+ return deviceOn;
+ }
+
+ public long getOnTime() {
+ return onTime;
+ }
+
+ public int getSaturation() {
+ return saturation;
+ }
+
+ public long getTimeUsagePast7() {
+ return timeUsagePast7;
+ }
+
+ public long getTimeUsagePast30() {
+ return timeUsagePast30;
+ }
+
+ public long getTimeUsagePastToday() {
+ return timeUsageToday;
+ }
+
+ public TapoBulbModeEnum getWorkingMode() {
+ if (dynamicLightEffectEnable) {
+ return TapoBulbModeEnum.LIGHT_FX;
+ } else if (colorTemp == 0) {
+ return TapoBulbModeEnum.COLOR_LIGHT;
+ } else {
+ return TapoBulbModeEnum.WHITE_LIGHT;
+ }
+ }
+
+ public boolean supportsMultiRequest() {
+ return !getHardwareVersion().startsWith("1");
+ }
+
+ @Override
+ public String toString() {
+ return toJson();
+ }
+
+ public String toJson() {
+ return GSON.toJson(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.bulb;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoLightDynamicFx;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBulbHandler extends TapoBaseDeviceHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoBulbHandler.class);
+ private TapoBulbData bulbData = new TapoBulbData();
+ private TapoBulbLastStates lastStates = new TapoBulbLastStates();
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoBulbHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ @Override
+ public void newDataResult(String queryCommand) {
+ super.newDataResult(queryCommand);
+ if (DEVICE_CMD_GETINFO.equals(queryCommand)) {
+ bulbData = connector.getResponseData(TapoBulbData.class);
+ lastStates.put(bulbData.getWorkingMode(), bulbData);
+ updateChannels(bulbData);
+ }
+ }
+
+ /*****************************
+ * HANDLE COMMANDS
+ *****************************/
+
+ /**
+ * handle command sent to device
+ *
+ * @param channelUID channelUID command is sent to
+ * @param command command to be sent
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String channel = channelUID.getIdWithoutGroup();
+ if (command instanceof RefreshType) {
+ queryDeviceData();
+ } else {
+ switch (channel) {
+ case CHANNEL_OUTPUT:
+ handleOnOffCommand(command);
+ break;
+ case CHANNEL_BRIGHTNESS:
+ handleBrightnessCommand(command);
+ break;
+ case CHANNEL_COLOR_TEMP:
+ handleColorTempCommand(command);
+ break;
+ case CHANNEL_COLOR:
+ handleColorCommand(command);
+ break;
+ case CHANNEL_FX_NAME:
+ handleLightFx(command);
+ break;
+ case CHANNEL_MODE:
+ handleModeChange(command);
+ break;
+ default:
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command,
+ channelUID.getId());
+ }
+ }
+ }
+
+ private void handleOnOffCommand(Command command) {
+ switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+ }
+
+ private void handleBrightnessCommand(Command command) {
+ if (command instanceof PercentType percentCommand) {
+ Float percent = percentCommand.floatValue();
+ setBrightness(percent.intValue()); // 0..100% = 0..100
+ } else if (command instanceof DecimalType decimalCommand) {
+ setBrightness(decimalCommand.intValue());
+ }
+ }
+
+ private void handleColorCommand(Command command) {
+ if (command instanceof HSBType hsbCommand) {
+ setColor(hsbCommand);
+ }
+ }
+
+ private void handleColorTempCommand(Command command) {
+ if (command instanceof DecimalType decimalCommand) {
+ setColorTemp(decimalCommand.intValue());
+ }
+ }
+
+ private void handleLightFx(Command command) {
+ setLightEffect(command.toString());
+ }
+
+ private void handleModeChange(Command command) {
+ setLastMode(TapoBulbModeEnum.valueOf(command.toString()));
+ }
+
+ /*****************************
+ * SEND COMMANDS
+ *****************************/
+
+ /**
+ * Switch device On or Off
+ *
+ * @param on if true device will switch on. Otherwise switch off
+ */
+ protected void switchOnOff(boolean on) {
+ bulbData.switchOnOff(on);
+ connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+ }
+
+ /**
+ * Set Britghtness of device
+ *
+ * @param newBrightness percentage 0-100 of new brightness
+ */
+ protected void setBrightness(Integer newBrightness) {
+ /* switch off if 0 */
+ if (newBrightness == 0) {
+ bulbData.switchOff();
+ } else {
+ bulbData.switchOn();
+ bulbData.setBrightness(newBrightness);
+ }
+ connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+ }
+
+ /**
+ * Set Color of Device
+ *
+ * @param command HSBType
+ */
+ protected void setColor(HSBType command) {
+ bulbData.switchOn();
+ bulbData.setColorTemp(0);
+ bulbData.setHue(command.getHue().intValue());
+ bulbData.setSaturation(command.getSaturation().intValue());
+ bulbData.setBrightness(command.getBrightness().intValue());
+ connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+ }
+
+ /**
+ * Set ColorTemp
+ *
+ * @param colorTemp (Integer) in Kelvin
+ */
+ protected void setColorTemp(Integer colorTemp) {
+ bulbData.switchOn();
+ bulbData.setHue(0);
+ bulbData.setColorTemp(colorTemp);
+ connector.sendCommandAndQuery(bulbData, bulbData.supportsMultiRequest());
+ }
+
+ /**
+ * Set light effect
+ *
+ * @param fxId (String) id of LightEffect
+ */
+ protected void setLightEffect(String fxId) {
+ try {
+ TapoLightDynamicFx fxData = new TapoLightDynamicFx();
+ fxData.setEffect(fxId);
+ connector.sendCommandAndQuery(DEVICE_CMD_SET_DYNAIMCLIGHT_FX, fxData, bulbData.supportsMultiRequest());
+ } catch (TapoErrorHandler te) {
+ logger.warn("({}) could not load effect '{}' - {}", uid, fxId, te.getMessage());
+ }
+ }
+
+ /**
+ * Set last state by mode
+ *
+ * @param mode mode to set
+ */
+ protected void setLastMode(TapoBulbModeEnum mode) {
+ TapoBulbData lastState = lastStates.get(mode);
+ if (TapoBulbModeEnum.LIGHT_FX.equals(mode)) {
+ setLightEffect(lastState.getDynamicLightEffectId());
+ } else {
+ connector.sendCommandAndQuery(lastState, bulbData.supportsMultiRequest());
+ }
+ }
+
+ /*****************************
+ * UPDATE CHANNELS
+ *****************************/
+
+ protected void updateChannels(TapoBulbData deviceData) {
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceData.isOn()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_MODE),
+ getStringType(deviceData.getWorkingMode().toString()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+ getPercentType(deviceData.getBrightness()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+ getDecimalType(deviceData.getColorTemp()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceData.getHSB());
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+ getDecimalType(deviceData.getSignalLevel()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+ getTimeType(deviceData.getOnTime(), Units.SECOND));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceData.isOverheated()));
+
+ updateLightEffectChannels(deviceData);
+ }
+
+ /**
+ * Update light effect channels
+ */
+ protected void updateLightEffectChannels(TapoBulbData deviceData) {
+ String fxId = "";
+ if (deviceData.dynamicLightEffectEnabled()) {
+ fxId = deviceData.getDynamicLightEffectId();
+ }
+ updateState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(fxId));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.bulb;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+
+import java.util.EnumMap;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * List with Tapo-Bulb Information LastState
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoBulbLastStates {
+ private EnumMap<TapoBulbModeEnum, @Nullable TapoBulbData> lastModeData = new EnumMap<>(TapoBulbModeEnum.class);
+
+ /**
+ * INIT
+ */
+ public TapoBulbLastStates() {
+ lastModeData.put(TapoBulbModeEnum.UNKOWN, new TapoBulbData());
+ }
+
+ /**
+ * Return LastModeData of Mode
+ *
+ * @param mode mode to get
+ * @return
+ */
+ public TapoBulbData get(TapoBulbModeEnum mode) {
+ if (lastModeData.containsKey(mode)) {
+ TapoBulbData lastmode = lastModeData.get(mode);
+ if (lastmode != null) {
+ return lastmode;
+ }
+ }
+ return getDefaultData(mode);
+ }
+
+ /**
+ * Set LastmodeData for Mode
+ *
+ * @param mode mode to set
+ * @param data actual state-data
+ */
+ public void put(TapoBulbModeEnum mode, TapoBulbData data) {
+ lastModeData.put(mode, data);
+ }
+
+ /**
+ * Get DefaultData
+ *
+ * @param mode mode to get
+ * @return
+ */
+ private TapoBulbData getDefaultData(TapoBulbModeEnum mode) {
+ TapoBulbData defaultData = new TapoBulbData();
+ defaultData.setBrightness(100);
+ defaultData.setSaturation(100);
+ defaultData.switchOnOff(true);
+ defaultData.setDynamicLightEffectId("L1");
+
+ if (TapoBulbModeEnum.WHITE_LIGHT.equals(mode)) {
+ defaultData.setColorTemp(BULB_MAX_COLORTEMP - BULB_MIN_COLORTEMP);
+ }
+ return defaultData;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.bulb;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Tapo-Bulb-Mode Enum
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public enum TapoBulbModeEnum {
+ UNKOWN,
+ WHITE_LIGHT,
+ COLOR_LIGHT,
+ LIGHT_FX;
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.hub;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Hub Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoHubData extends TapoBaseDeviceData {
+ @SerializedName("in_alarm")
+ @Expose(serialize = false, deserialize = true)
+ private boolean alarmActive = false;
+
+ @SerializedName("in_alarm_source")
+ @Expose(serialize = false, deserialize = true)
+ private String alarmSource = "";
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+ public boolean alarmIsActive() {
+ return alarmActive;
+ }
+
+ public String getAlarmSource() {
+ return alarmSource;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.hub;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildList;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.discovery.TapoChildDiscoveryService;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TapoHubHandler} is responsible for handling commands, which are
+ * sent to the child devices of the hub with a bridge.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoHubHandler extends TapoBaseDeviceHandler implements BridgeHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoHubHandler.class);
+ protected TapoHubData hubData = new TapoHubData();
+ private TapoChildList tapoChildsList = new TapoChildList();
+ private @NonNullByDefault({}) TapoChildDiscoveryService discoveryService;
+ private List<Thing> tapoChildThings = new ArrayList<>();
+
+ public TapoHubHandler(Thing thing) {
+ super(thing);
+ logger.trace("{} Hub initialized", uid);
+ }
+
+ /**
+ * Activate Device
+ */
+ @Override
+ protected void activateDevice() {
+ super.activateDevice();
+ discoveryService.setBackGroundDiscovery(deviceConfig.backgroundDiscovery);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("{} Hub doesn't handle command: {}", uid, command);
+ }
+
+ @Override
+ public void dispose() {
+ logger.trace("{} Hub disposed ", uid);
+ super.dispose();
+ }
+
+ @Override
+ public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
+ logger.trace("({}) childHandlerInitialized '{}'", uid, childThing.getUID());
+ tapoChildThings.add(childThing);
+ }
+
+ @Override
+ public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
+ logger.trace("({}) childHandlerDisposed '{}'", uid, childThing.getUID());
+ tapoChildThings.remove(childThing);
+ }
+
+ /***********************************
+ *
+ * CHILD DISCOVERY SERVICE
+ *
+ ************************************/
+
+ /**
+ * ACTIVATE DISCOVERY SERVICE
+ */
+
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Set.of(TapoChildDiscoveryService.class);
+ }
+
+ /**
+ * Set DiscoveryService
+ *
+ * @param discoveryService
+ */
+ public void setDiscoveryService(TapoChildDiscoveryService discoveryService) {
+ discoveryService.setBackGroundDiscovery(deviceConfig.backgroundDiscovery);
+ this.discoveryService = discoveryService;
+ }
+
+ /****************************
+ * PUBLIC FUNCTIONS
+ ****************************/
+
+ /**
+ * query device Properties
+ */
+ @Override
+ public void queryDeviceData() {
+ deviceError.reset();
+ if (isLoggedIn(LOGIN_RETRIES)) {
+ List<TapoRequest> requests = new ArrayList<>();
+ requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+ requests.add(new TapoRequest(DEVICE_CMD_GETCHILDDEVICELIST));
+ connector.sendMultipleRequest(requests);
+ }
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ @Override
+ public void newDataResult(String queryCommand) {
+ super.newDataResult(queryCommand);
+ switch (queryCommand) {
+ case DEVICE_CMD_GETINFO:
+ hubData = connector.getResponseData(TapoHubData.class);
+ updateChannels(hubData);
+ break;
+ case DEVICE_CMD_GETCHILDDEVICELIST:
+ tapoChildsList = connector.getResponseData(TapoChildList.class);
+ updateChildDevices(tapoChildsList);
+ break;
+ default:
+ responsePasstrough(connector.getResponseData(TapoResponse.class));
+ break;
+ }
+ }
+
+ /****************************
+ * CHILD THINGS
+ ****************************/
+
+ /**
+ * Update all Child-Things
+ */
+ public void updateChildThings() {
+ for (Thing thing : tapoChildThings) {
+ updateChild(thing);
+ }
+ }
+
+ /**
+ * Update Child single child with special representationProperty
+ *
+ * @param thingTypeToUpdate ThingTypeUID of Thing to update
+ * @param representationProperty Name of representationProperty
+ * @param propertyValue Value of representationProperty
+ */
+ public void updateChild(ThingTypeUID thingTypeToUpdate, String representationProperty, String propertyValue) {
+ for (Thing thing : tapoChildThings) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+ if (thingTypeToUpdate.equals(thingTypeUID)) {
+ String thingProperty = thing.getProperties().get(representationProperty);
+ if (propertyValue.equals(thingProperty)) {
+ updateChild(thing);
+ }
+ }
+ }
+ }
+
+ /**
+ * Update Child-Thing (send refreshCommand)
+ *
+ * @param thing - Thing to update
+ */
+ public void updateChild(Thing thing) {
+ ThingHandler handler = thing.getHandler();
+ if (handler != null) {
+ ChannelUID cUid = new ChannelUID(thing.getUID(), "any");
+ handler.handleCommand(cUid, RefreshType.REFRESH);
+ }
+ }
+
+ /**
+ * Set State of all clients
+ *
+ * @param thingStatus new ThingStatus
+ */
+ public void updateChildStates(ThingStatus thingStatus) {
+ for (Thing thing : tapoChildThings) {
+ updateChildState(thing, thingStatus);
+ }
+ }
+
+ /**
+ * Set State of a Thing
+ *
+ * @param thing Thing to update
+ * @param thingStatus new ThingStatus
+ */
+ public void updateChildState(Thing thing, ThingStatus thingStatus) {
+ logger.trace("{} set child states to {} by hub", uid, thingStatus);
+ ThingHandler handler = thing.getHandler();
+ if (handler != null) {
+ if (ThingStatus.OFFLINE.equals(thingStatus)) {
+ handler.bridgeStatusChanged(new ThingStatusInfo(thingStatus, ThingStatusDetail.BRIDGE_OFFLINE, ""));
+ } else {
+ handler.bridgeStatusChanged(new ThingStatusInfo(thingStatus, ThingStatusDetail.NONE, ""));
+ }
+ }
+ }
+
+ /****************************
+ * UPDATE HUB CHANNELS
+ ****************************/
+
+ /**
+ * Update Channels
+ */
+ protected void updateChannels(TapoHubData hubData) {
+ updateState(getChannelID(CHANNEL_GROUP_ALARM, CHANNEL_ALARM_ACTIVE), getOnOffType(hubData.alarmIsActive()));
+ updateState(getChannelID(CHANNEL_GROUP_ALARM, CHANNEL_ALARM_SOURCE), getStringType(hubData.getAlarmSource()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+ getDecimalType(hubData.getSignalLevel()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(hubData.isOverheated()));
+ }
+
+ /**
+ * Update channels of childdevices
+ */
+ protected void updateChildDevices(TapoChildList childList) {
+ tapoChildsList = childList;
+ if (discoveryService.isBackgroundDiscoveryEnabled()) {
+ discoveryService.thingsDiscovered(childList.getChildDeviceList());
+ }
+ /* update children */
+ updateChildThings();
+ }
+
+ /****************************
+ * HUB GETTERS
+ ****************************/
+
+ public ThingUID getUID() {
+ return getThing().getUID();
+ }
+
+ public List<TapoChildDeviceData> getChildDevices() {
+ return tapoChildsList.getChildDeviceList();
+ }
+
+ public TapoChildDeviceData getChild(String deviceSerial) {
+ List<TapoChildDeviceData> childDeviceList = tapoChildsList.getChildDeviceList();
+ for (int i = 0; i <= childDeviceList.size(); i++) {
+ TapoChildDeviceData child = childDeviceList.get(i);
+ if (child.getDeviceId().equals(deviceSerial)) {
+ return child;
+ }
+ }
+ logger.debug("child not found in deviceList '{}'", deviceSerial);
+ return new TapoChildDeviceData();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.lightstrip;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoLightEffect;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightStripData extends TapoBaseDeviceData {
+ @SerializedName("device_on")
+ @Expose(serialize = true, deserialize = true)
+ private boolean deviceOn = false;
+
+ @Expose(serialize = true, deserialize = true)
+ private int brightness = 0;
+
+ @SerializedName("color_temp")
+ @Expose(serialize = true, deserialize = true)
+ private int colorTemp = 0;
+
+ @Expose(serialize = true, deserialize = true)
+ private int hue = 0;
+
+ @Expose(serialize = true, deserialize = true)
+ private int saturation = 100;
+
+ @SerializedName("on_time")
+ @Expose(serialize = false, deserialize = true)
+ private long onTime = 0;
+
+ @SerializedName("music_rhythm_enable")
+ @Expose(serialize = true, deserialize = true)
+ private boolean musicRythmEnable = false;
+
+ @SerializedName("music_rhythm_mode")
+ @Expose(serialize = true, deserialize = true)
+ private String musicRythmMode = "";
+
+ @SerializedName("lighting_effect")
+ @Expose(serialize = false, deserialize = true)
+ private TapoLightEffect lightingEffect = new TapoLightEffect();
+
+ /***********************************
+ *
+ * SET VALUES
+ *
+ ************************************/
+ public void switchOn() {
+ deviceOn = true;
+ }
+
+ public void switchOff() {
+ deviceOn = false;
+ }
+
+ public void switchOnOff(boolean on) {
+ deviceOn = on;
+ }
+
+ public void setBrightness(int value) {
+ if (lightingEffect.isEnabled()) {
+ lightingEffect.setBrightness(value);
+ } else {
+ brightness = value;
+ }
+ }
+
+ public void setColorTemp(int value) {
+ colorTemp = value;
+ }
+
+ public void setHue(int value) {
+ hue = value;
+ }
+
+ public void setSaturation(int value) {
+ saturation = value;
+ }
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public int getBrightness() {
+ if (lightingEffect.isEnabled()) {
+ return lightingEffect.getBrightness();
+ } else {
+ return brightness;
+ }
+ }
+
+ public int getColorTemp() {
+ return colorTemp;
+ }
+
+ public HSBType getHSB() {
+ DecimalType h = new DecimalType(hue);
+ PercentType s = new PercentType(saturation);
+ PercentType b = new PercentType(brightness);
+ return new HSBType(h, s, b);
+ }
+
+ public int getHue() {
+ return hue;
+ }
+
+ public TapoLightEffect getLightEffect() {
+ return lightingEffect;
+ }
+
+ public boolean isOff() {
+ return !deviceOn;
+ }
+
+ public boolean isOn() {
+ return deviceOn;
+ }
+
+ public int getSaturation() {
+ return saturation;
+ }
+
+ public Number getOnTime() {
+ return onTime;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.lightstrip;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoLightEffect;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoLightStripHandler extends TapoBaseDeviceHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoLightStripHandler.class);
+ private TapoLightStripData lightStripData = new TapoLightStripData();
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoLightStripHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ @Override
+ public void newDataResult(String queryCommand) {
+ super.newDataResult(queryCommand);
+ if (DEVICE_CMD_GETINFO.equals(queryCommand)) {
+ lightStripData = connector.getResponseData(TapoLightStripData.class);
+ updateChannels(lightStripData);
+ }
+ }
+
+ /*****************************
+ * HANDLE COMMANDS
+ *****************************/
+
+ /**
+ * handle command sent to device
+ *
+ * @param channelUID channelUID command is sent to
+ * @param command command to be sent
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String channel = channelUID.getIdWithoutGroup();
+ String group = channelUID.getGroupId();
+ if (command instanceof RefreshType) {
+ queryDeviceData();
+ } else if (CHANNEL_GROUP_EFFECTS.equals(group)) {
+ handleLightFx(channel, command);
+ } else {
+ switch (channel) {
+ case CHANNEL_OUTPUT:
+ handleOnOffCommand(command);
+ break;
+ case CHANNEL_BRIGHTNESS:
+ if (lightStripData.getLightEffect().isEnabled()) {
+ handleLightFx(channel, command);
+ } else {
+ handleBrightnessCommand(command);
+ }
+ break;
+ case CHANNEL_COLOR_TEMP:
+ handleColorTempCommand(command);
+ break;
+ case CHANNEL_COLOR:
+ handleColorCommand(command);
+ break;
+ default:
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command,
+ channelUID.getId());
+ }
+ }
+ }
+
+ private void handleOnOffCommand(Command command) {
+ switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+ }
+
+ private void handleBrightnessCommand(Command command) {
+ if (command instanceof PercentType percentCommand) {
+ Float percent = percentCommand.floatValue();
+ setBrightness(percent.intValue()); // 0..100% = 0..100
+ } else if (command instanceof DecimalType decimalCommand) {
+ setBrightness(decimalCommand.intValue());
+ }
+ }
+
+ private void handleColorCommand(Command command) {
+ if (command instanceof HSBType hsbCommand) {
+ setColor(hsbCommand);
+ }
+ }
+
+ private void handleColorTempCommand(Command command) {
+ if (command instanceof DecimalType decimalCommand) {
+ setColorTemp(decimalCommand.intValue());
+ }
+ }
+
+ private void handleLightFx(String channel, Command command) {
+ TapoLightEffect lightEffect = lightStripData.getLightEffect();
+ switch (channel) {
+ case CHANNEL_BRIGHTNESS:
+ if (command instanceof PercentType percentCommand) {
+ Float percent = percentCommand.floatValue();
+ lightEffect.setBrightness(percent.intValue()); // 0..100% = 0..100
+ } else if (command instanceof DecimalType decimalCommand) {
+ lightEffect.setBrightness(decimalCommand.intValue());
+ }
+ break;
+ case CHANNEL_FX_NAME:
+ try {
+ lightEffect = lightEffect.setEffect(command.toString());
+ } catch (Exception e) {
+ logger.warn("({}) could not load effect '{}'", uid, command);
+ }
+
+ break;
+ default:
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channel);
+ }
+ setLightEffect(lightEffect);
+ }
+
+ /*****************************
+ * SEND COMMANDS
+ *****************************/
+
+ /**
+ * Switch device On or Off
+ *
+ * @param on if true device will switch on. Otherwise switch off
+ */
+ protected void switchOnOff(boolean on) {
+ lightStripData.switchOnOff(on);
+ connector.sendCommandAndQuery(lightStripData, true);
+ }
+
+ /**
+ * Set Britghtness of device
+ *
+ * @param newBrightness percentage 0-100 of new brightness
+ */
+ protected void setBrightness(Integer newBrightness) {
+ /* switch off if 0 */
+ if (newBrightness == 0) {
+ lightStripData.switchOff();
+ } else {
+ lightStripData.switchOn();
+ lightStripData.setBrightness(newBrightness);
+ }
+ connector.sendCommandAndQuery(lightStripData, true);
+ }
+
+ /**
+ * Set Color of Device
+ *
+ * @param command HSBType
+ */
+ protected void setColor(HSBType command) {
+ lightStripData.switchOn();
+ lightStripData.setHue(command.getHue().intValue());
+ lightStripData.setSaturation(command.getSaturation().intValue());
+ lightStripData.setBrightness(command.getBrightness().intValue());
+ connector.sendCommandAndQuery(lightStripData, true);
+ }
+
+ /**
+ * Set ColorTemp
+ *
+ * @param colorTemp (Integer) in Kelvin
+ */
+ protected void setColorTemp(Integer colorTemp) {
+ lightStripData.switchOn();
+ lightStripData.setColorTemp(colorTemp);
+ connector.sendCommandAndQuery(lightStripData, true);
+ }
+
+ /**
+ * Set light effect
+ *
+ * @param lightEffect TapoLightEffect
+ */
+ protected void setLightEffect(TapoLightEffect lightEffect) {
+ connector.sendDeviceCommand(DEVICE_CMD_SET_LIGHT_FX, lightEffect);
+ }
+
+ /*****************************
+ * UPDATE CHANNELS
+ *****************************/
+
+ protected void updateChannels(TapoLightStripData deviceData) {
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceData.isOn()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+ getPercentType(deviceData.getBrightness()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR_TEMP),
+ getDecimalType(deviceData.getColorTemp()));
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_COLOR), deviceData.getHSB());
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+ getDecimalType(deviceData.getSignalLevel()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+ getTimeType(deviceData.getOnTime(), Units.SECOND));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceData.isOverheated()));
+
+ updateLightEffectChannels(deviceData);
+ }
+
+ /**
+ * Update light effect channels
+ */
+ protected void updateLightEffectChannels(TapoLightStripData deviceData) {
+ TapoLightEffect lightEffect = deviceData.getLightEffect();
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_BRIGHTNESS),
+ getPercentType(deviceData.getBrightness()));
+ updateState(getChannelID(CHANNEL_GROUP_EFFECTS, CHANNEL_FX_NAME), getStringType(lightEffect.getName()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.socket;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Tapo-Device Information class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSocketData extends TapoBaseDeviceData {
+ @SerializedName("device_on")
+ @Expose(serialize = true, deserialize = true)
+ private boolean deviceOn = false;
+
+ @SerializedName("on_time")
+ @Expose(serialize = false, deserialize = true)
+ private long onTime = 0;
+
+ @SerializedName("time_usage_past7")
+ @Expose(serialize = false, deserialize = true)
+ private long timeUsagePast7 = 0;
+
+ @SerializedName("time_usage_past30")
+ @Expose(serialize = false, deserialize = true)
+ private long timeUsagePast30 = 0;
+
+ @SerializedName("time_usage_today")
+ @Expose(serialize = false, deserialize = true)
+ private long timeUsageToday = 0;
+
+ /***********************************
+ *
+ * SET VALUES
+ *
+ ************************************/
+
+ public void switchOn() {
+ deviceOn = true;
+ }
+
+ public void switchOff() {
+ deviceOn = false;
+ }
+
+ public void switchOnOff(boolean on) {
+ deviceOn = on;
+ }
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ public boolean isOff() {
+ return !deviceOn;
+ }
+
+ public boolean isOn() {
+ return deviceOn;
+ }
+
+ public Number getOnTime() {
+ return onTime;
+ }
+
+ public long getTimeUsagePast7() {
+ return timeUsagePast7;
+ }
+
+ public long getTimeUsagePast30() {
+ return timeUsagePast30;
+ }
+
+ public long getTimeUsagePastToday() {
+ return timeUsageToday;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.socket;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSocketHandler extends TapoBaseDeviceHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoSocketHandler.class);
+ private TapoSocketData socketData = new TapoSocketData();
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoSocketHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ @Override
+ public void newDataResult(String queryCommand) {
+ super.newDataResult(queryCommand);
+ switch (queryCommand) {
+ case DEVICE_CMD_GETINFO:
+ socketData = connector.getResponseData(TapoSocketData.class);
+ updateChannels(socketData);
+ break;
+ default:
+ responsePasstrough(connector.getResponseData(TapoResponse.class));
+ break;
+ }
+ }
+
+ /*****************************
+ * HANDLE COMMANDS
+ *****************************/
+
+ /**
+ * handle command sent to device
+ *
+ * @param channelUID channelUID command is sent to
+ * @param command command to be sent
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ /* perform actions */
+ if (command instanceof RefreshType) {
+ queryDeviceData();
+ } else if (command instanceof OnOffType) {
+ handleOnOffCommand(command);
+ } else {
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+ }
+ }
+
+ private void handleOnOffCommand(Command command) {
+ switchOnOff(command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE);
+ }
+
+ /*****************************
+ * SEND COMMANDS
+ *****************************/
+
+ /**
+ * Switch device On or Off
+ *
+ * @param on if true device will switch on. Otherwise switch off
+ */
+ protected void switchOnOff(boolean on) {
+ socketData.switchOnOff(on);
+ connector.sendCommandAndQuery(socketData, true);
+ }
+
+ /*****************************
+ * UPDATE CHANNELS
+ *****************************/
+
+ protected void updateChannels(TapoSocketData deviceData) {
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT), getOnOffType(deviceData.isOn()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+ getDecimalType(deviceData.getSignalLevel()));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_ONTIME),
+ getTimeType(deviceData.getOnTime(), Units.SECOND));
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT), getOnOffType(deviceData.isOverheated()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.devices.wifi.socket;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TypeUtils.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoBaseDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildList;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoBaseDeviceHandler;
+import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * TAPO Smart-Plug-Device.
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoSocketStripHandler extends TapoBaseDeviceHandler {
+ private final Logger logger = LoggerFactory.getLogger(TapoSocketStripHandler.class);
+ private TapoChildList tapoChildList = new TapoChildList();
+ private TapoSocketData socketData = new TapoSocketData();
+
+ /**
+ * Constructor
+ *
+ * @param thing Thing object representing device
+ */
+ public TapoSocketStripHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Function called by {@link org.openhab.binding.tapocontrol.internal.api.TapoDeviceConnector} if new data were
+ * received
+ *
+ * @param queryCommand command where new data belong to
+ */
+ @Override
+ public void newDataResult(String queryCommand) {
+ super.newDataResult(queryCommand);
+ switch (queryCommand) {
+ case DEVICE_CMD_GETINFO:
+ baseDeviceData = connector.getResponseData(TapoBaseDeviceData.class);
+ updateChannels(baseDeviceData);
+ break;
+ case DEVICE_CMD_GETCHILDDEVICELIST:
+ tapoChildList = connector.getResponseData(TapoChildList.class);
+ updateChildDevices(tapoChildList);
+ break;
+ default:
+ logger.warn("({}) unhandled queryCommand '{}'", uid, queryCommand);
+ }
+ }
+
+ /**
+ * query device Properties
+ */
+ @Override
+ public void queryDeviceData() {
+ deviceError.reset();
+ if (isLoggedIn(LOGIN_RETRIES)) {
+ List<TapoRequest> requests = new ArrayList<>();
+ requests.add(new TapoRequest(DEVICE_CMD_GETINFO));
+ requests.add(new TapoRequest(DEVICE_CMD_GETCHILDDEVICELIST));
+ connector.sendMultipleRequest(requests);
+ }
+ }
+
+ /**
+ * Get ChildData by position (index)
+ */
+ private Optional<TapoChildDeviceData> getChildData(int position) {
+ return tapoChildList.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
+ }
+
+ /*****************************
+ * HANDLE COMMANDS
+ *****************************/
+
+ /**
+ * handle command sent to device
+ *
+ * @param channelUID channelUID command is sent to
+ * @param command command to be sent
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ /* perform actions */
+ if (command instanceof RefreshType) {
+ queryDeviceData();
+ } else if (command instanceof OnOffType) {
+ handleOnOffCommand(channelUID, command);
+ } else {
+ logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command, channelUID.getId());
+ }
+ }
+
+ private void handleOnOffCommand(ChannelUID channelUID, Command command) {
+ String id = channelUID.getIdWithoutGroup();
+ Boolean targetState = command == OnOffType.ON ? Boolean.TRUE : Boolean.FALSE;
+ if (id.startsWith(CHANNEL_OUTPUT)) { // Command is sent to a child's device output
+ Integer index = Integer.valueOf(id.replace(CHANNEL_OUTPUT, ""));
+ switchOnOff(targetState, index);
+ }
+ }
+
+ /*****************************
+ * SEND COMMANDS
+ *****************************/
+
+ /**
+ * Switch child On or Off
+ *
+ * @param on if true device will switch on. Otherwise switch off
+ * @param index index of child device
+ */
+ protected void switchOnOff(boolean on, int index) {
+ socketData.switchOnOff(on);
+
+ getChildData(index).ifPresent(child -> {
+ child.setDeviceOn(on);
+ connector.sendChildCommand(child, true);
+ });
+ }
+
+ /*****************************
+ * UPDATE CHANNELS
+ *****************************/
+
+ /**
+ * Set all device Childs data to device
+ */
+ public void updateChildDevices(TapoChildList hostData) {
+ hostData.getChildDeviceList().forEach(child -> {
+ updateChildDevice(child.getPosition(), child);
+ });
+ }
+
+ /**
+ * Set data to child with index (position) x
+ */
+ public void updateChildDevice(int index, TapoChildDeviceData child) {
+ updateState(getChannelID(CHANNEL_GROUP_ACTUATOR, CHANNEL_OUTPUT + index), getOnOffType(child.isOn()));
+ }
+
+ /**
+ * Update Channels
+ */
+ protected void updateChannels(TapoBaseDeviceData deviceData) {
+ updateState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_WIFI_STRENGTH),
+ getDecimalType(deviceData.getSignalLevel()));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.discovery;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.hub.TapoHubHandler;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO Smart Home thing discovery
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoChildDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+ private final Logger logger = LoggerFactory.getLogger(TapoChildDiscoveryService.class);
+ protected @NonNullByDefault({}) TapoHubHandler hub;
+ private boolean backgroundDiscoveryEnabled = false;
+ private String uid = "";
+
+ /***********************************
+ *
+ * INITIALIZATION
+ *
+ ************************************/
+
+ /**
+ * INIT CLASS
+ *
+ */
+ public TapoChildDiscoveryService() {
+ super(SUPPORTED_HUB_CHILD_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
+ }
+
+ /**
+ * deactivate
+ */
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ logger.trace("({}) DiscoveryService deactivated", uid);
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof TapoHubHandler hubHandler) {
+ TapoHubHandler tapoHub = hubHandler;
+ tapoHub.setDiscoveryService(this);
+ hub = tapoHub;
+ uid = hub.getUID().toString();
+ activate();
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return this.hub;
+ }
+
+ /**
+ * Enable or disable backgrounddiscovery
+ */
+ public void setBackGroundDiscovery(boolean enableService) {
+ backgroundDiscoveryEnabled = enableService;
+ if (enableService) {
+ super.startBackgroundDiscovery();
+ } else {
+ super.stopBackgroundDiscovery();
+ }
+ }
+
+ @Override
+ public boolean isBackgroundDiscoveryEnabled() {
+ return backgroundDiscoveryEnabled;
+ }
+
+ /***********************************
+ *
+ * SCAN HANDLING
+ *
+ ************************************/
+
+ /**
+ * Start scan manually
+ */
+ @Override
+ public void startScan() {
+ removeOlderResults(getTimestampOfLastScan());
+ if (hub != null) {
+ logger.trace("({}) DiscoveryService scan started", uid);
+ hub.queryDeviceData();
+ }
+ }
+
+ @Override
+ public synchronized void stopScan() {
+ super.stopScan();
+ thingsDiscovered(hub.getChildDevices());
+ logger.trace("({}) DiscoveryService scan stoped", uid);
+ }
+
+ /***********************************
+ *
+ * handle Results
+ *
+ ************************************/
+
+ /*
+ * create discoveryResults and discovered things
+ */
+ public void thingsDiscovered(List<TapoChildDeviceData> resultData) {
+ resultData.forEach(child -> {
+ ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, child.getModel());
+ if (SUPPORTED_HUB_CHILD_TYPES_UIDS.contains(thingTypeUID)) {
+ DiscoveryResult discoveryResult = createResult(child);
+ thingDiscovered(discoveryResult);
+ } else {
+ logger.debug("({}) Discovered unsupportet ThingType '{}'", uid, thingTypeUID);
+ }
+ });
+ }
+
+ /**
+ * create discoveryResult (Thing) from TapoChild Object
+ */
+ public DiscoveryResult createResult(TapoChildDeviceData child) {
+ TapoHubHandler tapoHub = this.hub;
+ String deviceModel = child.getModel();
+ String deviceSerial = child.getDeviceId();
+ String label = getDeviceLabel(child);
+ ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+ /* create properties */
+ Map<String, Object> properties = new HashMap<>();
+ properties.put(Thing.PROPERTY_VENDOR, DEVICE_VENDOR);
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, formatMac(child.getMAC(), MAC_DIVISION_CHAR));
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION, child.getFirmwareVersion());
+ properties.put(Thing.PROPERTY_HARDWARE_VERSION, child.getHardwareVersion());
+ properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, deviceSerial);
+
+ logger.debug("({}) device of type '{}' discovered with serial'{}'", uid, deviceModel, deviceSerial);
+ if (tapoHub != null) {
+ ThingUID bridgeUID = tapoHub.getUID();
+ ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceSerial);
+ return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withRepresentationProperty(CHILD_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
+ .build();
+ } else {
+ ThingUID thingUID = new ThingUID(BINDING_ID, deviceSerial);
+ return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withRepresentationProperty(CHILD_REPRESENTATION_PROPERTY).withLabel(label).build();
+ }
+ }
+
+ /**
+ * Get devicelabel from from TapoChild Object
+ */
+ protected String getDeviceLabel(TapoChildDeviceData child) {
+ try {
+ String deviceLabel = "";
+ String deviceModel = child.getModel();
+ ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+ if (SUPPORTED_SMART_CONTACTS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_SMART_CONTACT;
+ } else if (SUPPORTED_MOTION_SENSORS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_MOTION_SENSOR;
+ }
+ return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
+ } catch (Exception e) {
+ logger.debug("({}) error getDeviceLabel", uid, e);
+ return "";
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.discovery;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.api.TapoCloudConnector;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeConfiguration;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.devices.wifi.TapoDeviceConfiguration;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResult;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResultList;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler class for TAPO Smart Home thing discovery
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
+ private final Logger logger = LoggerFactory.getLogger(TapoDiscoveryService.class);
+ private final String uid;
+ protected @NonNullByDefault({}) TapoBridgeHandler bridge;
+ protected @NonNullByDefault({}) TapoUdpDiscovery udpDiscovery;
+ protected @NonNullByDefault({}) TapoCloudConnector cloudConnector;
+ private @NonNullByDefault({}) TapoBridgeConfiguration config;
+ private @Nullable ScheduledFuture<?> discoveryJob;
+ private TapoDiscoveryResultList discoveryResultList = new TapoDiscoveryResultList();
+
+ /***********************************
+ *
+ * INITIALIZATION
+ *
+ ************************************/
+
+ public TapoDiscoveryService() {
+ super(SUPPORTED_THING_TYPES_UIDS, TAPO_DISCOVERY_TIMEOUT_S, false);
+ uid = "Discovery-Service";
+ }
+
+ @Override
+ public void activate() {
+ config = bridge.getBridgeConfig();
+ if (config.cloudDiscovery || config.udpDiscovery) {
+ startBackgroundDiscovery();
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ stopScheduler(discoveryJob);
+ }
+
+ @Override
+ public void startBackgroundDiscovery() {
+ startDiscoveryScheduler();
+ }
+
+ @Override
+ public void stopBackgroundDiscovery() {
+ stopScheduler(discoveryJob);
+ }
+
+ /**
+ * setThingHandler - set bridge and other handlers on initializing service
+ */
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof TapoBridgeHandler bridgeHandler) {
+ TapoBridgeHandler tapoBridge = bridgeHandler;
+ tapoBridge.setDiscoveryService(this);
+ bridge = tapoBridge;
+ udpDiscovery = new TapoUdpDiscovery(bridge);
+ cloudConnector = bridgeHandler.getCloudConnector();
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return this.bridge;
+ }
+
+ /*************************
+ * SCHEDULER
+ *************************/
+ /**
+ * Start DeviceDiscovery Scheduler
+ */
+ protected void startDiscoveryScheduler() {
+ config = bridge.getBridgeConfig();
+ int pollingInterval = config.discoveryInterval;
+ TimeUnit timeUnit = TimeUnit.MINUTES;
+ if ((config.cloudDiscovery || config.udpDiscovery) && pollingInterval > 0) {
+ logger.debug("{} starting discoveryScheduler with interval {} {}", this.uid, pollingInterval, timeUnit);
+
+ this.discoveryJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, pollingInterval, timeUnit);
+ } else {
+ logger.debug("({}) discoveryScheduler disabled with config '0'", uid);
+ stopScheduler(this.discoveryJob);
+ }
+ }
+
+ /**
+ * Stop scheduler
+ *
+ * @param scheduler ScheduledFeature which should be stopped
+ */
+ protected void stopScheduler(@Nullable ScheduledFuture<?> scheduler) {
+ if (scheduler != null) {
+ scheduler.cancel(true);
+ scheduler = null;
+ }
+ }
+
+ /***********************************
+ *
+ * SCAN HANDLING
+ *
+ ************************************/
+
+ /**
+ * Start scan manually
+ */
+ @Override
+ public void startScan() {
+ logger.trace("{} starting scan", this.uid);
+ removeOlderResults(getTimestampOfLastScan());
+ discoveryResultList.clear();
+ try {
+ if (bridge != null) {
+ bridge.getErrorHandler().reset();
+ if (config.cloudDiscovery && bridge.loginCloud()) {
+ cloudConnector.getDeviceList();
+ }
+ if (config.udpDiscovery && udpDiscovery != null) {
+ udpDiscovery.startScan();
+ }
+ handleDiscoveryList(discoveryResultList);
+ }
+ } catch (Exception e) {
+ logger.debug("({}) scan failed: ", uid, e);
+ }
+ }
+
+ @Override
+ public void abortScan() {
+ logger.trace("{} scan aborted", this.uid);
+ }
+
+ @Override
+ public void stopScan() {
+ logger.trace("{} scan stoped", this.uid);
+ }
+
+ /***********************************
+ *
+ * handle Results
+ *
+ ************************************/
+
+ /*
+ * add scan results to discoveryResultList
+ */
+ public void addScanResults(TapoDiscoveryResultList deviceList) {
+ if (discoveryResultList.size() == 0) {
+ discoveryResultList = deviceList;
+ } else {
+ for (TapoDiscoveryResult result : deviceList) {
+ addScanResult(result);
+ }
+ }
+ }
+
+ /**
+ * add singleResult to list
+ */
+ public void addScanResult(TapoDiscoveryResult device) {
+ discoveryResultList.addResult(device);
+ }
+
+ /**
+ * work with result from get devices from deviceList
+ *
+ * @param deviceList
+ */
+ protected void handleDiscoveryList(TapoDiscoveryResultList deviceList) {
+ logger.trace("{} handle discovery result", this.uid);
+ try {
+ for (TapoDiscoveryResult deviceElement : deviceList) {
+ if (!deviceElement.deviceMac().isBlank()) {
+ String deviceModel = getDeviceModel(deviceElement);
+ String deviceLabel = getDeviceLabel(deviceElement);
+ ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+ /* create thing */
+ if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
+ if (!config.onlyLocalOnlineDevices || deviceElement.ip().length() > 1) {
+ DiscoveryResult discoveryResult = createResult(deviceElement);
+ thingDiscovered(discoveryResult);
+ } else {
+ logger.debug("{} device discovered but is offline '{}'", this.uid, deviceLabel);
+ }
+ } else {
+ logger.debug("{} unsupported device discovered '{}'", this.uid, deviceLabel);
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.debug("({}) error handle DiscoveryResult", uid, e);
+ }
+ }
+
+ /**
+ * CREATE DISCOVERY RESULT
+ * creates discoveryResult (Thing) from JsonObject got from Cloud
+ *
+ * @param device JsonObject with device information
+ * @return DiscoveryResult-Object
+ */
+ private DiscoveryResult createResult(TapoDiscoveryResult device) {
+ TapoBridgeHandler tapoBridge = this.bridge;
+ String deviceModel = getDeviceModel(device);
+ String label = getDeviceLabel(device);
+ String deviceMAC = device.deviceMac();
+ ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+ /* create properties */
+ Map<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.fwVer());
+ properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.deviceHwVer());
+ properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.deviceId());
+ if (device.ip().length() >= 7) {
+ properties.put(TapoDeviceConfiguration.CONFIG_DEVICE_IP, device.ip());
+ properties.put(TapoDeviceConfiguration.CONFIG_HTTP_PORT, device.encryptionShema().httpPort());
+ properties.put(TapoDeviceConfiguration.CONFIG_PROTOCOL, device.encryptionShema().encryptType());
+ }
+
+ logger.debug("device {} discovered with mac {}", deviceModel, deviceMAC);
+ if (tapoBridge != null) {
+ ThingUID bridgeUID = tapoBridge.getUID();
+ ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
+ return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
+ .build();
+ } else {
+ ThingUID thingUID = new ThingUID(BINDING_ID, deviceMAC);
+ return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withLabel(label).build();
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.discovery;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+import java.util.Random;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
+import org.openhab.binding.tapocontrol.internal.devices.bridge.TapoBridgeHandler;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResult;
+import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoKeyPair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Handler class for TAPO Smart Home device UDP-connections.
+ * THIS IS FOR TESTING
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoUdpDiscovery {
+ private final Logger logger = LoggerFactory.getLogger(TapoUdpDiscovery.class);
+ private final TapoBridgeHandler bridge;
+ private String uid;
+
+ private static final Integer BROADCAST_TIMEOUT_MS = 8000;
+ private static final int[] BROADCAST_DISCOVERY_PORTS = { 9999, 20002 };
+ private static final int TDP_CHECKSUM_DEFAULT = 1516993677;
+ private static final int TDP_RANDOM_BOUND = 268435456;
+ private static final int BUFFER_SIZE = 2048;
+ private static final int RSA_KEYSIZE = 2048;
+ private static final String RSA_MESSAGE_KEY = "rsa_key";
+ private static final String RSA_MESSAGE_START_BYTES = "0200000100001100";
+
+ public TapoUdpDiscovery(TapoBridgeHandler bridge) {
+ this.bridge = bridge;
+ uid = bridge.getUID().getAsString() + " - updDiscovery";
+ }
+
+ /***********************************
+ * SCAN HANDLING
+ ************************************/
+
+ /**
+ * start scan with configured broadcast address
+ */
+ protected void startScan() throws TapoErrorHandler {
+ String broadcastAddress = bridge.getBridgeConfig().broadcastAddress;
+ try {
+ scanAllPorts(InetAddress.getByName(broadcastAddress));
+ } catch (UnknownHostException e) {
+ logger.debug("({}) unknown host exception {}", uid, broadcastAddress);
+ throw new TapoErrorHandler(TapoErrorCode.ERR_CONFIG_IP, "unknown broadcast address");
+ }
+ }
+
+ /**
+ * Scan all available interfaces and ports
+ */
+ protected void scanAllInterfaces() throws TapoErrorHandler {
+ List<InetAddress> broadcastList = listAllBroadcastAddresses();
+ for (InetAddress inetAddress : broadcastList) {
+ scanAllPorts(inetAddress);
+ }
+ }
+
+ /**
+ * Scan all defined ports of an specific broadcastAddress
+ *
+ * @param broadcastAddress
+ */
+ protected void scanAllPorts(InetAddress broadcastAddress) throws TapoErrorHandler {
+ for (int port : BROADCAST_DISCOVERY_PORTS) {
+ udpScan(getBroadcastMessage(port), broadcastAddress, port);
+ }
+ }
+
+ /**
+ *
+ * @param sendData
+ * @param broadcastAddress
+ * @param discoveryPort
+ * @throws TapoErrorHandler
+ */
+ private void udpScan(byte[] message, InetAddress broadcastAddress, int discoveryPort) throws TapoErrorHandler {
+ logger.trace("({}) startUdpScan with address: '{}' to port {}", uid, broadcastAddress, discoveryPort);
+ try {
+ DatagramSocket udpSocket = new DatagramSocket();
+ udpSocket.setSoTimeout(BROADCAST_TIMEOUT_MS);
+ udpSocket.setBroadcast(true);
+
+ logger.trace("({}) send broadcast ('{}' byte) packet '{}'", uid, message.length, byteArrayToHex(message));
+
+ DatagramPacket sendPacket = new DatagramPacket(message, message.length, broadcastAddress, discoveryPort);
+
+ udpSocket.send(sendPacket);
+
+ while (!udpSocket.isClosed()) {
+ // Wait for a response
+ byte[] recvBuf = new byte[BUFFER_SIZE];
+ DatagramPacket receivePacket;
+ try {
+ receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
+ udpSocket.receive(receivePacket);
+ handleDiscoveryResult(receivePacket);
+ } catch (SocketTimeoutException e) {
+ logger.trace("({}) socketTimeOutException", uid);
+ udpSocket.close();
+ }
+ }
+ udpSocket.close();
+ } catch (Exception e) {
+ logger.debug("({}) scan failed: '{}'", uid, e.getMessage());
+ throw new TapoErrorHandler(e);
+ }
+ }
+
+ /***********************************
+ * PRIVATE HELPERS
+ ************************************/
+
+ /*
+ * List all Broadcast-Addresses from all Interfaces
+ */
+ private List<InetAddress> listAllBroadcastAddresses() throws TapoErrorHandler {
+ try {
+ List<InetAddress> broadcastList = new ArrayList<>();
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ while (interfaces.hasMoreElements()) {
+ @Nullable
+ NetworkInterface networkInterface = interfaces.nextElement();
+ if (networkInterface.isLoopback() || !networkInterface.isUp()) {
+ continue;
+ }
+ networkInterface.getInterfaceAddresses().stream().map(a -> a.getBroadcast()).filter(Objects::nonNull)
+ .forEach(broadcastList::add);
+ }
+ return broadcastList;
+ } catch (Exception e) {
+ logger.debug("({}) socket exception", uid);
+ throw new TapoErrorHandler(e);
+ }
+ }
+
+ /**
+ *
+ * @param port
+ * @return
+ */
+ private byte[] getBroadcastMessage(int port) throws TapoErrorHandler {
+ String message;
+ byte[] messageBytes = {};
+ try {
+ switch (port) {
+ case 9999:
+ // message = "'system': {'get_sysinfo': None}"; // unencrypted message
+ message = "d0f281f88bff9af7d5ef94b6d1b4c09fec95e68fe187e8caf08bf68ba785e688cba7c8bdd9fbc1ba98ff9aeeb1d8b6d0bf9da7dca1dcf0d2a1ccaddfabc7aec8ad83ea85f1dfbcd3bed3bcd2fc9ff39ce98daf95eeccabcebae58ce284ebc9f388f588a486f598f98bff93fa9cf9d7b4d5b896ff8fec8de085f796b8dbb7d8adc9ebd1aa88ef8afea1c8a6c0af8db7ccb1ccb1";
+ messageBytes = hexStringToByteArray(message);
+ break;
+ case 20002:
+ messageBytes = buildRsaPacket();
+ break;
+ case 20004:
+ messageBytes = buildRsaPacket();
+ break;
+ }
+ return messageBytes;
+ } catch (Exception e) {
+ throw new TapoErrorHandler(e);
+ }
+ }
+
+ /*
+ * build discoverypacket with rsa key-message
+ */
+ private byte[] buildRsaPacket() throws TapoErrorHandler {
+ String message;
+
+ /* build jsonObject with rsa key */
+ JsonObject parameters = new JsonObject();
+ JsonObject messageObject = new JsonObject();
+
+ parameters.addProperty(RSA_MESSAGE_KEY, new TapoKeyPair(RSA_KEYSIZE).getPublicKey());
+ messageObject.add("params", parameters);
+ message = messageObject.toString();
+ logger.trace("({}) discovery-message: '{}'", uid, message);
+
+ return buildByteMessage(message);
+ }
+
+ /*
+ * build message prefix dynamic insert bytes that change based on the string, randomness and crc
+ * algorithm found by reverse-engineering android app in com\tplink\tdp\common\b.java
+ */
+ private byte[] buildByteMessage(String message) throws TapoErrorHandler {
+ byte[] replace;
+ byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
+ byte[] fullPacket = new byte[messageBytes.length + 16];
+
+ /* put startvalues and message */
+ System.arraycopy(hexStringToByteArray(RSA_MESSAGE_START_BYTES), 0, fullPacket, 0, 8);
+ System.arraycopy(messageBytes, 0, fullPacket, 16, messageBytes.length);
+
+ /* replace with message lenght */
+ replace = shortToByteArray((short) messageBytes.length, ByteOrder.BIG_ENDIAN);
+ System.arraycopy(replace, 0, fullPacket, 4, 2);
+
+ /* replace with a random array */
+ replace = intToByteArray(new Random().nextInt(TDP_RANDOM_BOUND) + 0, ByteOrder.BIG_ENDIAN);
+ System.arraycopy(replace, 0, fullPacket, 8, 4);
+
+ /* replace with checksum default */
+ replace = intToByteArray(TDP_CHECKSUM_DEFAULT, ByteOrder.BIG_ENDIAN);
+ System.arraycopy(replace, 0, fullPacket, 12, 4);
+
+ /* replace crc */
+ replace = intToByteArray((int) crc32Checksum(fullPacket), ByteOrder.BIG_ENDIAN);
+ System.arraycopy(replace, 0, fullPacket, 12, 4);
+
+ return fullPacket;
+ }
+
+ /**
+ * Handle discoveryresult and add to resultList
+ *
+ * @param receivePacket
+ */
+ private void handleDiscoveryResult(DatagramPacket receivePacket) {
+ String responseMessage = new String(receivePacket.getData(), StandardCharsets.UTF_8);
+ logger.trace("({}) received responseMessage: '{}'", uid, responseMessage);
+ responseMessage = responseMessage.substring(responseMessage.indexOf("{")).trim();
+ TapoResponse tapoResponse = getObjectFromJson(responseMessage, TapoResponse.class);
+ bridge.getDiscoveryService().addScanResult(getObjectFromJson(tapoResponse.result(), TapoDiscoveryResult.class));
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.discovery.dto;
+
+import static org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolEnum.*;
+import static org.openhab.binding.tapocontrol.internal.helpers.TapoEncoder.isBase64Encoded;
+
+import java.util.Base64;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * TapoDiscoveryResult Data Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoDiscoveryResult(@Expose @SerializedName("factory_default") boolean factoryDefault,
+ @Expose @SerializedName("is_support_iot_cloud") boolean isSupportIOT,
+ @Expose @SerializedName("mgt_encrypt_schm") EncryptionShema encryptionShema, @Expose int role,
+ @Expose int status, @Expose String alias, @Expose String appServerUrl, @Expose String deviceHwVer,
+ @Expose @SerializedName(value = "deviceID", alternate = "device_id") String deviceId,
+ @Expose @SerializedName(value = "deviceMac", alternate = "mac") String deviceMac,
+ @Expose @SerializedName(value = "deviceModel", alternate = "device_model") String deviceModel,
+ @Expose String deviceName, @Expose String deviceRegion,
+ @Expose @SerializedName(value = "deviceType", alternate = "device_type") String deviceType, @Expose String fwId,
+ @Expose String fwVer, @Expose String hwId, @Expose String ip, @Expose String isSameRegion,
+ @Expose String oemId) {
+
+ public record EncryptionShema(@Expose @SerializedName("is_support_https") boolean isSupportHttps,
+ @Expose @SerializedName("encrypt_type") String encryptType,
+ @Expose @SerializedName("http_port") int httpPort, @Expose int lv2) {
+ }
+
+ /* init new emty record */
+
+ public TapoDiscoveryResult() {
+ this(false, false, new EncryptionShema(false, SECUREPASSTROUGH.toString(), 80, 0), 0, 0, "", "", "", "", "", "",
+ "", "", "", "", "", "", "", "", "");
+ }
+
+ /**********************************************
+ * Return default data if recordobject is null
+ **********************************************/
+
+ @Override
+ public boolean factoryDefault() {
+ return Objects.requireNonNullElse(factoryDefault, false);
+ }
+
+ @Override
+ public boolean isSupportIOT() {
+ return Objects.requireNonNullElse(isSupportIOT, false);
+ }
+
+ @Override
+ public int role() {
+ return Objects.requireNonNullElse(role, 0);
+ }
+
+ @Override
+ public int status() {
+ return Objects.requireNonNullElse(status, 0);
+ }
+
+ @Override
+ public String alias() {
+ String encodedAlias = Objects.requireNonNullElse(alias, "");
+
+ if (isBase64Encoded(encodedAlias)) {
+ return new String(Base64.getDecoder().decode(encodedAlias));
+ } else {
+ return alias;
+ }
+ }
+
+ @Override
+ public String appServerUrl() {
+ return Objects.requireNonNullElse(appServerUrl, "");
+ }
+
+ @Override
+ public String deviceHwVer() {
+ return Objects.requireNonNullElse(deviceHwVer, "");
+ }
+
+ @Override
+ public String deviceId() {
+ return Objects.requireNonNullElse(deviceId, "");
+ }
+
+ @Override
+ public String deviceMac() {
+ String mac = Objects.requireNonNullElse(deviceMac, "");
+ return TapoUtils.unformatMac(mac).toUpperCase();
+ }
+
+ @Override
+ public String deviceModel() {
+ return Objects.requireNonNullElse(deviceModel, "");
+ }
+
+ @Override
+ public String deviceName() {
+ return Objects.requireNonNullElse(deviceName, "");
+ }
+
+ @Override
+ public String deviceRegion() {
+ return Objects.requireNonNullElse(deviceRegion, "");
+ }
+
+ @Override
+ public String deviceType() {
+ return Objects.requireNonNullElse(deviceType, "");
+ }
+
+ @Override
+ public EncryptionShema encryptionShema() {
+ return Objects.requireNonNullElse(encryptionShema,
+ new EncryptionShema(false, SECUREPASSTROUGH.toString(), 80, 0));
+ }
+
+ @Override
+ public String fwId() {
+ return Objects.requireNonNullElse(fwId, "");
+ }
+
+ @Override
+ public String fwVer() {
+ return Objects.requireNonNullElse(fwVer, "");
+ }
+
+ @Override
+ public String hwId() {
+ return Objects.requireNonNullElse(hwId, "");
+ }
+
+ @Override
+ public String ip() {
+ return Objects.requireNonNullElse(ip, "");
+ }
+
+ @Override
+ public String isSameRegion() {
+ return Objects.requireNonNullElse(isSameRegion, "");
+ }
+
+ @Override
+ public String oemId() {
+ return Objects.requireNonNullElse(oemId, "");
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.discovery.dto;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * TapoCloud DeviceList Data Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoDiscoveryResultList implements Iterable<TapoDiscoveryResult> {
+
+ @Expose
+ private List<TapoDiscoveryResult> deviceList = new ArrayList<>(0);
+
+ /* init new emty list */
+ public TapoDiscoveryResultList() {
+ }
+
+ /**
+ * Add device to devicelist. Overwrite fields of device with new data if already exists and field has values
+ *
+ * @param result
+ */
+ public void addResult(TapoDiscoveryResult result) {
+ TapoDiscoveryResult old = getDeviceByMac(result.deviceMac());
+ if (old == null) {
+ deviceList.add(result);
+ } else {
+ /* create new discoveryresult and overwrite empty values */
+ boolean factoryDefault = compareValuesAgainstComparator(old.factoryDefault(), result.factoryDefault(),
+ false);
+ boolean isSupportIOT = compareValuesAgainstComparator(old.isSupportIOT(), result.isSupportIOT(), false);
+ TapoDiscoveryResult.EncryptionShema enctyptionShema = compareValuesAgainstComparator(old.encryptionShema(),
+ result.encryptionShema(), new TapoDiscoveryResult.EncryptionShema(false, "", 0, 0));
+ int role = compareValuesAgainstComparator(old.role(), result.role(), 0);
+ int status = compareValuesAgainstComparator(old.status(), result.status(), 0);
+ String alias = compareValuesAgainstComparator(old.alias(), result.alias(), "");
+ String appServerUrl = compareValuesAgainstComparator(old.alias(), result.alias(), "");
+ String deviceHwVer = compareValuesAgainstComparator(old.deviceHwVer(), result.deviceHwVer(), "");
+ String deviceId = compareValuesAgainstComparator(old.deviceId(), result.deviceId(), "");
+ String deviceMac = compareValuesAgainstComparator(old.deviceMac(), result.deviceMac(), "");
+ String deviceModel = compareValuesAgainstComparator(old.deviceModel(), result.deviceModel(), "");
+ String deviceRegion = compareValuesAgainstComparator(old.deviceRegion(), result.deviceRegion(), "");
+ String deviceType = compareValuesAgainstComparator(old.deviceType(), result.deviceType(), "");
+ String fwId = compareValuesAgainstComparator(old.fwId(), result.fwId(), "");
+ String fwVer = compareValuesAgainstComparator(old.fwVer(), result.fwVer(), "");
+ String hwId = compareValuesAgainstComparator(old.hwId(), result.hwId(), "");
+ String ip = compareValuesAgainstComparator(old.ip(), result.ip(), "");
+ String isSameRegion = compareValuesAgainstComparator(old.isSameRegion(), result.isSameRegion(), "");
+ String oemId = compareValuesAgainstComparator(old.oemId(), result.oemId(), "");
+
+ /* add new result */
+ deviceList.remove(old);
+ deviceList.add(new TapoDiscoveryResult(factoryDefault, isSupportIOT, enctyptionShema, role, status, alias,
+ appServerUrl, deviceHwVer, deviceId, deviceMac, deviceModel, deviceModel, deviceRegion, deviceType,
+ fwId, fwVer, hwId, ip, isSameRegion, oemId));
+ }
+ }
+
+ public void clear() {
+ deviceList = new ArrayList<>(0);
+ }
+
+ /*
+ * check if list contains element with mac
+ */
+ public boolean containsDeviceWithMac(final String mac) {
+ return deviceList.stream().anyMatch(o -> mac.equals(o.deviceMac()));
+ }
+
+ /*
+ * return element which contains mac
+ */
+ public @Nullable TapoDiscoveryResult getDeviceByMac(final String mac) {
+ return deviceList.stream().filter(o -> mac.equals(o.deviceMac())).findFirst().orElse(null);
+ }
+
+ public List<TapoDiscoveryResult> deviceList() {
+ return Objects.requireNonNullElse(deviceList, List.of());
+ }
+
+ @Override
+ public Iterator<TapoDiscoveryResult> iterator() {
+ return deviceList.iterator();
+ }
+
+ public int size() {
+ return deviceList.size();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.dto;
+
+/**
+ * Interface for main requests sent to devices
+ *
+ * @author Christian Wild - Initial contribution
+ */
+public interface TapoBaseRequestInterface {
+ public String method();
+
+ public Object params();
+
+ public long requestTimeMils();
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.devices.dto.TapoChildDeviceData;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+import io.reactivex.annotations.Nullable;
+
+/**
+ * Holds child data sent to device
+ *
+ * @author Gaël L'hopital - Initial contribution
+ * @author Christian Wild - Code revision
+ */
+@NonNullByDefault
+public record TapoChildRequest(@Expose String method, @Expose @Nullable Object params,
+ @Expose long requestTimeMils) implements TapoBaseRequestInterface {
+
+ private record ControlRequest(@Expose @SerializedName("device_id") String deviceId, @Expose Object requestData) {
+ }
+
+ /**
+ * Create request to control child devices
+ */
+ public TapoChildRequest(TapoChildDeviceData deviceData) {
+ this(DEVICE_CMD_CONTROL_CHILD,
+ new ControlRequest(deviceData.getDeviceId(), new TapoRequest(DEVICE_CMD_SETINFO, deviceData)),
+ System.currentTimeMillis());
+ }
+
+ /***********************************************
+ * RETURN VALUES
+ **********************************************/
+
+ @Override
+ public String toString() {
+ return toJson();
+ }
+
+ public String toJson() {
+ return GSON.toJson(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoComConstants.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.Expose;
+
+import io.reactivex.annotations.Nullable;
+
+/**
+ * Holds multi-request-data sent to device
+ *
+ * @author Christian Wild - Changed SubRequest top MultiRequest
+ */
+@NonNullByDefault
+public record TapoMultipleRequest(@Expose String method, @Expose @Nullable Object params,
+ @Expose long requestTimeMils) implements TapoBaseRequestInterface {
+
+ public record SubRequest(@Expose List<TapoRequest> requests) {
+ }
+
+ public TapoMultipleRequest(TapoRequest... requests) {
+ this(DEVICE_CMD_MULTIPLE_REQ, new SubRequest(Arrays.asList(requests)), System.currentTimeMillis());
+ }
+
+ public TapoMultipleRequest(List<TapoRequest> requests) {
+ this(DEVICE_CMD_MULTIPLE_REQ, new SubRequest(requests), System.currentTimeMillis());
+ }
+
+ /***********************************************
+ * RETURN VALUES
+ **********************************************/
+
+ @Override
+ public String toString() {
+ return toJson();
+ }
+
+ public String toJson() {
+ return GSON.toJson(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.Expose;
+
+import io.reactivex.annotations.Nullable;
+
+/**
+ * Holds data sent to device
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoRequest(@Expose String method, @Expose @Nullable Object params,
+ @Expose long requestTimeMils) implements TapoBaseRequestInterface {
+
+ /**
+ * Create request with command (method) and data (params) sent to device
+ */
+ public TapoRequest(String method, Object params) {
+ this(method, params, System.currentTimeMillis());
+ }
+
+ /**
+ * Create request with command (method) sent to device
+ */
+ public TapoRequest(String method) {
+ this(method, "", System.currentTimeMillis());
+ }
+
+ /***********************************************
+ * RETURN VALUES
+ **********************************************/
+
+ @Override
+ public String toString() {
+ return toJson();
+ }
+
+ public String toJson() {
+ return new GsonBuilder().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation().create().toJson(this);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.dto;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Tapo-Response Structure Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public record TapoResponse(@Expose @SerializedName(value = "errorcode", alternate = "error_code") int errorCode,
+ @Expose @SerializedName("result") JsonObject result, @Expose String method,
+ @Expose @SerializedName("msg") String message) {
+
+ private static final String MULTI_RESPONSE_KEY = "responses";
+
+ public TapoResponse() {
+ this(0, new JsonObject(), "", "");
+ }
+
+ public TapoResponse(int errorCode) {
+ this(errorCode, new JsonObject(), "", "");
+ }
+
+ /***********************************************
+ * RETURN VALUES
+ * Return default data if recordobject is null
+ **********************************************/
+
+ public boolean hasError() {
+ return errorCode != 0;
+ }
+
+ public boolean isMultiRequestResponse() {
+ return result.has(MULTI_RESPONSE_KEY);
+ }
+
+ @Override
+ public int errorCode() {
+ return Objects.requireNonNullElse(errorCode, ERR_API_CLOUD_FAILED.getCode());
+ }
+
+ @Override
+ public JsonObject result() {
+ return Objects.requireNonNullElse(result, new JsonObject());
+ }
+
+ @Override
+ public String message() {
+ return Objects.requireNonNullElse(message, "");
+ }
+
+ @Override
+ public String method() {
+ return Objects.requireNonNullElse(method, "");
+ }
+
+ public List<TapoResponse> responses() {
+ JsonArray responses = result.getAsJsonArray(MULTI_RESPONSE_KEY);
+ Type repsonseListType = new TypeToken<List<TapoResponse>>() {
+ }.getType();
+ return Objects.requireNonNullElse(GSON.fromJson(responses.toString(), repsonseListType),
+ new ArrayList<TapoResponse>());
+ }
+
+ @Override
+ public String toString() {
+ return toJson();
+ }
+
+ public String toJson() {
+ return new GsonBuilder().disableHtmlEscaping().excludeFieldsWithoutExposeAnnotation().create().toJson(this);
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.helpers;
-
-import static java.util.Base64.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * MimeEncoder
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class MimeEncode {
-
- public byte[] encode(byte[] src) {
- return getMimeEncoder().encode(src);
- }
-
- public String encodeToString(byte[] src) {
- return getMimeEncoder().encodeToString(src);
- }
-
- public byte[] decode(byte[] src) {
- return getMimeDecoder().decode(src);
- }
-
- public byte[] decode(String src) {
- return getMimeDecoder().decode(src);
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.helpers;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-
-/**
- * PAYLOAD BUILDER
- * Generates payload for TapoHttp request
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class PayloadBuilder {
- public String method = "";
- private JsonObject parameters = new JsonObject();
-
- /**
- * Set Command
- *
- * @param command command (method) to send
- */
- public void setCommand(String command) {
- this.method = command;
- }
-
- /**
- * Add Parameter
- *
- * @param name parameter name
- * @param value parameter value (typeOf Bool,Number or String)
- */
- public void addParameter(String name, Object value) {
- if (value instanceof Boolean bool) {
- this.parameters.addProperty(name, bool);
- } else if (value instanceof Number number) {
- this.parameters.addProperty(name, number);
- } else {
- this.parameters.addProperty(name, value.toString());
- }
- }
-
- /**
- * Get JSON Payload (STRING)
- *
- * @return String JSON-Payload
- */
- public String getPayload() {
- Gson gson = new Gson();
- JsonObject payload = getJsonPayload();
- return gson.toJson(payload);
- }
-
- /**
- * Get JSON Payload (JSON-Object)
- *
- * @return JsonObject JSON-Payload
- */
- public JsonObject getJsonPayload() {
- JsonObject payload = new JsonObject();
- long timeMils = System.currentTimeMillis();// * 1000;
-
- payload.addProperty("method", this.method);
- if (this.parameters.size() > 0) {
- payload.add("params", this.parameters);
- }
- payload.addProperty("requestTimeMils", timeMils);
-
- return payload;
- }
-
- /**
- * Flush Parameters
- * remove all parameters
- */
- public void flushParameters(String command) {
- parameters = new JsonObject();
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.helpers;
-
-import 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
- */
- 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);
- }
-}
*/
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 = "";
+public record TapoCredentials(String username, String password) {
- /**
- * INIT CLASS
- *
- */
public TapoCredentials() {
- this.mimeEncoder = new MimeEncode();
- }
-
- /**
- * INIT CLASS
- *
- * @param eMail E-Mail-adress of Tapo Cloud
- * @param password Password of Tapo Cloud
- */
- public TapoCredentials(String eMail, String password) {
- this.mimeEncoder = new MimeEncode();
- setCredectials(eMail, password);
- }
-
- /**
- * set credentials.
- *
- * @param eMail username (eMail-adress) of Tapo Cloud
- * @param password Password of Tapo Cloud
- */
- public void setCredectials(String eMail, String password) {
- try {
- this.username = eMail;
- this.password = password;
- encryptCredentials(eMail, password);
- createKeyPair();
- } catch (Exception e) {
- logger.warn("error init credential class '{}'", e.toString());
- }
- }
-
- /**
- * encrypt credentials.
- *
- * @param username username (eMail-adress) of Tapo Cloud
- * @param passowrd Password of Tapo Cloud
- */
- private void encryptCredentials(String username, String password) throws Exception {
- logger.trace("encrypt credentials for '{}'", username);
-
- /* Password Encoding */
- byte[] byteWord = password.getBytes();
- this.encodedPassword = mimeEncoder.encodeToString(byteWord);
-
- /* User Encoding */
- String encodedUser = this.shaDigestUsername(username);
- byteWord = encodedUser.getBytes("UTF-8");
- this.encodedEmail = mimeEncoder.encodeToString(byteWord);
- }
-
- /**
- * Create Key-Pairs
- *
- */
- public void createKeyPair() throws NoSuchAlgorithmException {
- logger.trace("generating new keypair");
- KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
- instance.initialize(1024, new SecureRandom());
- KeyPair generateKeyPair = instance.generateKeyPair();
-
- this.publicKey = new String(mimeEncoder.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
- this.privateKey = new String(mimeEncoder.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
- logger.trace("new privateKey: '{}'", this.privateKey);
- logger.trace("new ublicKey: '{}'", this.publicKey);
- }
-
- /**
- * shaDigest USERNAME
- *
- */
- private String shaDigestUsername(String str) throws NoSuchAlgorithmException {
- byte[] bArr = str.getBytes();
- byte[] digest = MessageDigest.getInstance("SHA1").digest(bArr);
-
- StringBuilder sb = new StringBuilder();
- for (byte b : digest) {
- String hexString = Integer.toHexString(b & 255);
- if (hexString.length() == 1) {
- sb.append("0");
- sb.append(hexString);
- } else {
- sb.append(hexString);
- }
- }
- return sb.toString();
- }
-
- /**
- * RETURN ENCODED PASSWORD
- *
- */
- public String getEncodedPassword() {
- return encodedPassword;
- }
-
- /**
- * RETURN ENCODED E-MAIL
- *
- */
- public String getEncodedEmail() {
- return encodedEmail;
- }
-
- /**
- * RETURN PASSWORD
- *
- */
- public String getPassword() {
- return password;
- }
-
- /**
- * RETURN Username (E-MAIL)
- *
- */
- public String getUsername() {
- return username;
- }
-
- /**
- * RETURN PRIVATE-KEY
- *
- * @return String -----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----
- */
- public String getPrivateKey() {
- return String.format("-----BEGIN PRIVATE KEY-----%n%s%n-----END PRIVATE KEY-----%n", privateKey);
- }
-
- /**
- * RETURN PUBLIC KEY
- *
- * @return String -----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----
- */
- public String getPublicKey() {
- return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
- }
-
- /**
- * RETURN PRIVATE-KEY (BYTES)
- *
- * @return UTF-8 coded byte[] with private key
- */
- public byte[] getPrivateKeyBytes() {
- try {
- return privateKey.getBytes("UTF-8");
- } catch (Exception e) {
- return new byte[0];
- }
- }
-
- /**
- * RETURN PUBLIC-KEY (BYTES)
- *
- * @return UTF-8 coded byte[] with private key
- */
- public byte[] getPublicKeyBytes() {
- try {
- return publicKey.getBytes("UTF-8");
- } catch (Exception e) {
- return new byte[0];
- }
+ this("", "");
}
- /**
- * CHECK IF CREDENTIALS ARE SET
- *
- * @return
- */
- public Boolean areSet() {
- return !(this.username.isEmpty() || this.password.isEmpty());
+ public boolean areSet() {
+ return !username.isBlank() && !password.isBlank();
}
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers;
+
+import static java.util.Base64.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.zip.CRC32;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Class for Encoding, Crypting, and Decrypting
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoEncoder {
+ private static final String SHA1_ALGORITHM = "SHA1";
+ private static final String SHA256_ALGORITHM = "SHA256";
+
+ /* b64 encode */
+ public static String b64Encode(String textToEncode) {
+ byte[] message = null;
+ message = textToEncode.getBytes(StandardCharsets.UTF_8);
+ return getMimeEncoder().encodeToString(message);
+ }
+
+ /* b64 decode */
+ public static String b64Decode(String textToDecode) {
+ byte[] decoded = getMimeDecoder().decode(textToDecode);
+ return byteArrayToString(decoded);
+ }
+
+ /**
+ * Checks if a string is encoded to Base64.
+ *
+ * @param input The string to be checked.
+ * @return Returns true if the string is Base64 encoded, false otherwise.
+ */
+ public static boolean isBase64Encoded(String input) {
+ try {
+ byte[] decodedBytes = Base64.getDecoder().decode(input.getBytes());
+ String decodedString = new String(decodedBytes);
+ String reencodedString = Base64.getEncoder().encodeToString(decodedString.getBytes());
+ return input.equals(reencodedString);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /* create sha1 hash (string) */
+ public static String sha1Encode(String textToEncode) throws NoSuchAlgorithmException {
+ byte[] digest = sha1Encode(textToEncode.getBytes(StandardCharsets.UTF_8));
+ return byteArrayToString(digest);
+ }
+
+ /* create sha1 hash (byte[]) */
+ public static byte[] sha1Encode(byte[] bArr) throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(SHA1_ALGORITHM).digest(bArr);
+ }
+
+ /* create sha256 hash (string) */
+ public static String sha256Encode(String textToEncode) throws NoSuchAlgorithmException {
+ byte[] digest = textToEncode.getBytes(StandardCharsets.UTF_8);
+ return byteArrayToString(sha256Encode(digest));
+ }
+
+ /* create sha256 hash (byte[])) */
+ public static byte[] sha256Encode(byte[] bArr) throws NoSuchAlgorithmException {
+ return MessageDigest.getInstance(SHA256_ALGORITHM).digest(bArr);
+ }
+
+ /* get CRC32-Checksum from byteArray */
+ public static long crc32Checksum(byte[] bArr) {
+ CRC32 crc32 = new CRC32();
+ crc32.update(bArr);
+ return crc32.getValue();
+ }
+
+ /* convert bytearray[] into string */
+ public static String byteArrayToString(byte[] hexByte) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : hexByte) {
+ String hexString = Integer.toHexString(b & 255);
+ if (hexString.length() == 1) {
+ sb.append("0");
+ sb.append(hexString);
+ } else {
+ sb.append(hexString);
+ }
+ }
+ return sb.toString();
+ }
+}
import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
+import org.openhab.binding.tapocontrol.internal.constants.TapoErrorType;
/**
* Class Handling TapoErrors
*/
@NonNullByDefault
public class TapoErrorHandler extends Exception {
- private TapoErrorCode errorCode = TapoErrorCode.NO_ERROR;
private static final long serialVersionUID = 0L;
+ private Integer errorNumber = NO_ERROR.getCode();
private String infoMessage = "";
- private Gson gson = new Gson();
/**
* Constructor
* @param errorCode error code (number)
* @param infoMessage optional info-message
*/
- public TapoErrorHandler(Integer errorCode, String infoMessage) {
+ public TapoErrorHandler(Integer errorCode, @Nullable String infoMessage) {
raiseError(errorCode, infoMessage);
}
* @param ex Exception
* @param infoMessage optional info-message
*/
- public TapoErrorHandler(Exception ex, String infoMessage) {
+ public TapoErrorHandler(Exception ex, @Nullable String infoMessage) {
raiseError(ex, infoMessage);
}
* @param errorCode error code (TapoErrorCodeEnum)
* @param infoMessage optional info-message
*/
- public TapoErrorHandler(TapoErrorCode errorCode, String infoMessage) {
+ public TapoErrorHandler(TapoErrorCode errorCode, @Nullable String infoMessage) {
raiseError(errorCode, infoMessage);
}
*/
private String getErrorMessage(Integer errCode) {
String key = TapoErrorCode.fromCode(errCode).name().replace("ERR_", "error-").replace("_", "-").toLowerCase();
- return String.format("@text/%s [ \"%s\" ]", key, errCode.toString());
+ return String.format("@text/%s [ \"%s\" ]", key, errorNumber);
}
/***********************************
raiseError(errorCode, "");
}
- /**
- * Raises new error
- *
- * @param errorCode error code (number)
- * @param infoMessage optional info-message
- */
- public void raiseError(Integer errorCode, String infoMessage) {
- raiseError(TapoErrorCode.fromCode(errorCode), infoMessage);
- }
-
/**
* Raises new error
*
* @param ex Exception
* @param infoMessage optional info-message
*/
- public void raiseError(Exception ex, String infoMessage) {
- raiseError(TapoErrorCode.fromCode(ex.hashCode()), infoMessage);
+ public void raiseError(Exception ex, @Nullable String infoMessage) {
+ try {
+ throw ex;
+ } catch (TapoErrorHandler e) {
+ raiseError(e.getCode(), e.getMessagText());
+ } catch (TimeoutException e) {
+ raiseError(ERR_BINDING_CONNECT_TIMEOUT, infoMessage);
+ } catch (InterruptedException e) {
+ raiseError(ERR_BINDING_SEND_REQUEST, infoMessage);
+ } catch (ExecutionException e) {
+ raiseError(ERR_BINDING_SEND_REQUEST, infoMessage);
+ } catch (Exception e) {
+ raiseError(e.hashCode(), infoMessage);
+ }
}
/**
* @param errorCode error code (TapoErrorCodeEnum)
* @param infoMessage optional info-message
*/
- public void raiseError(TapoErrorCode errorCode, String infoMessage) {
- this.errorCode = errorCode;
- this.infoMessage = infoMessage;
+ public void raiseError(TapoErrorCode errorCode, @Nullable String infoMessage) {
+ raiseError(errorCode.getCode(), infoMessage);
+ }
+
+ /**
+ * Raises new error
+ *
+ * @param errorCode error code (number)
+ * @param infoMessage optional info-message
+ */
+ public void raiseError(Integer errorCode, @Nullable String infoMessage) {
+ errorNumber = errorCode;
+ if (infoMessage != null) {
+ this.infoMessage = infoMessage;
+ } else {
+ this.infoMessage = "";
+ }
}
/**
* @param tapoError
*/
public void set(TapoErrorHandler tapoError) {
- this.errorCode = TapoErrorCode.fromCode(tapoError.getCode());
- this.infoMessage = tapoError.getExtendedInfo();
+ errorNumber = tapoError.getCode();
+ infoMessage = tapoError.getExtendedInfo();
}
/**
* Reset Error
*/
public void reset() {
- this.errorCode = NO_ERROR;
- this.infoMessage = "";
+ errorNumber = NO_ERROR.getCode();
+ infoMessage = "";
}
/***********************************
@Override
@Nullable
public String getMessage() {
- return getErrorMessage(errorCode.getCode());
+ return getMessage(errorNumber);
+ }
+
+ public String getMessagText() {
+ return getMessage(errorNumber);
}
/**
* @return error code (integer)
*/
public Integer getCode() {
- return this.errorCode.getCode();
+ return errorNumber;
}
/**
* @return error extended info
*/
public String getExtendedInfo() {
- return this.infoMessage;
+ return infoMessage;
}
/**
* @return error code
*/
public TapoErrorCode getError() {
- return this.errorCode;
+ return TapoErrorCode.fromCode(errorNumber);
}
/**
*
* @return true if has error
*/
- public Boolean hasError() {
- return this.errorCode != NO_ERROR;
+ public boolean hasError() {
+ return !NO_ERROR.getCode().equals(errorNumber);
}
- /**
- * Get JSON-Object with errror
- *
- * @return JsonObject with error-informations
- */
- public JsonObject getJson() {
- JsonObject json;
- json = gson.fromJson(
- "{'error_code': '" + errorCode + "', 'error_message':'" + getErrorMessage(getCode()) + "'}",
- JsonObject.class);
- if (json == null) {
- json = new JsonObject();
- }
- return json;
+ public TapoErrorType getType() {
+ return TapoErrorCode.fromCode(errorNumber).getType();
+ }
+
+ @Override
+ public String toString() {
+ return getErrorMessage(getCode());
}
}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers;
+
+import static java.util.Base64.*;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Tapo RSA-KeyPair Class
+ *
+ * @author Christian Wild - Initial contribution
+ */
+@NonNullByDefault
+public class TapoKeyPair {
+ private static final String ALGORITHM_RSA = "RSA";
+ private static final String LINE_SEPARATOR = "\n";
+ private static final int LINE_LENGTH = 64;
+ private byte[] publicKeyBytes = {};
+ private byte[] privateKeyBytes = {};
+
+ public TapoKeyPair(int keysize) {
+ try {
+ createNewKeyPair(ALGORITHM_RSA, keysize);
+ } catch (Exception e) {
+ publicKeyBytes = new byte[0];
+ privateKeyBytes = new byte[0];
+ }
+ }
+
+ public TapoKeyPair(String algorithm, int keysize) throws NoSuchAlgorithmException {
+ createNewKeyPair(algorithm, keysize);
+ }
+
+ /**
+ * Create Key-Pairs
+ *
+ */
+ public void createNewKeyPair(String algorithm, int keysize) throws NoSuchAlgorithmException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
+ keyPairGenerator.initialize(keysize, new SecureRandom());
+ KeyPair generateKeyPair = keyPairGenerator.generateKeyPair();
+ Encoder mimEncoder = getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));
+
+ publicKeyBytes = mimEncoder.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded());
+ privateKeyBytes = mimEncoder.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded());
+ }
+
+ /***********************************
+ *
+ * GET VALUES
+ *
+ ************************************/
+
+ /**
+ * get PEM-formated Private-Key
+ *
+ * @return String -----BEGIN PRIVATE KEY-----\n%s\n-----END PRIVATE KEY-----
+ */
+ public String getPrivateKey() {
+ return String.format("-----BEGIN PRIVATE KEY-----%2$s%1$s%2$s-----END PRIVATE KEY-----%2$s",
+ new String(privateKeyBytes), LINE_SEPARATOR);
+ }
+
+ /**
+ * get PEM-formated Public-Key
+ *
+ * @return String -----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----
+ */
+ public String getPublicKey() {
+ return String.format("-----BEGIN PUBLIC KEY-----%2$s%1$s%2$s-----END PUBLIC KEY-----%2$s",
+ new String(publicKeyBytes), LINE_SEPARATOR);
+ }
+
+ /**
+ * get Private-Key as byte-array
+ *
+ * @return UTF-8 coded byte[] with private key
+ */
+ public byte[] getPrivateKeyBytes() {
+ return privateKeyBytes;
+ }
+
+ /**
+ * get Public-Key as byte-array
+ *
+ * @return UTF-8 coded byte[] with private key
+ */
+ public byte[] getPublicKeyBytes() {
+ return publicKeyBytes;
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.helpers;
-
-import javax.measure.Unit;
-import javax.measure.quantity.Energy;
-import javax.measure.quantity.Power;
-import javax.measure.quantity.Time;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.types.StringType;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-
-/**
- * {@link TapoUtils} TapoUtils -
- * Utility Helper Functions
- *
- * @author Christian Wild - Initial Initial contribution
- */
-@NonNullByDefault
-public class TapoUtils {
-
- /************************************
- * CALCULATION UTILS
- ***********************************/
- /**
- * Limit Value between limits
- *
- * @param value Integer
- * @param lowerLimit
- * @param upperLimit
- * @return
- */
- public static Integer limitVal(@Nullable Integer value, Integer lowerLimit, Integer upperLimit) {
- if (value == null || value < lowerLimit) {
- return lowerLimit;
- } else if (value > upperLimit) {
- return upperLimit;
- }
- return value;
- }
-
- /************************************
- * FORMAT UTILS
- ***********************************/
- /**
- * return value or default val if it's null
- *
- * @param <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);
- return unformatedMac.replaceAll("(.{2})", "$1" + newDivisionChar).substring(0, 17);
- }
-
- /**
- * unformat MAC-Address replace all division chars
- *
- * @param mac
- * @return
- */
- public static String unformatMac(String mac) {
- mac = mac.replace("-", "");
- mac = mac.replace(":", "");
- mac = mac.replace(".", "");
- return mac;
- }
-
- /**
- * HEX-STRING to byte convertion
- */
- public static byte[] hexStringToByteArray(String s) {
- int len = s.length();
- byte[] data = new byte[len / 2];
- try {
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
- }
- } catch (Exception e) {
- }
- return data;
- }
-
- /**
- * Return Boolean from string
- *
- * @param s - string to be converted
- * @param defVal - Default Value
- */
- public Boolean stringToBool(@Nullable String s, boolean defVal) {
- if (s == null) {
- return defVal;
- }
- try {
- return Boolean.parseBoolean(s);
- } catch (Exception e) {
- return defVal;
- }
- }
-
- /**
- * Return Integer from string
- *
- * @param s - string to be converted
- * @param defVal - Default Value
- */
- public Integer stringToInteger(@Nullable String s, Integer defVal) {
- if (s == null) {
- return defVal;
- }
- try {
- return Integer.valueOf(s);
- } catch (Exception e) {
- return defVal;
- }
- }
-
- /***********************************
- * JSON-FORMATER
- ************************************/
-
- public static boolean isValidJson(String json) {
- try {
- Gson gson = new Gson();
- JsonObject jsnObject = gson.fromJson(json, JsonObject.class);
- return jsnObject != null;
- } catch (Exception e) {
- return false;
- }
- }
-
- /**
- *
- * @param name parameter name
- * @param defVal - default value;
- * @return string value
- */
- public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name, String defVal) {
- if (jsonObject != null && jsonObject.has(name)) {
- return jsonObject.get(name).getAsString();
- } else {
- return defVal;
- }
- }
-
- /**
- *
- * @param name parameter name
- * @return string value
- */
- public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name) {
- return jsonObjectToString(jsonObject, name, "");
- }
-
- /**
- *
- * @param name parameter name
- * @param defVal - default value;
- * @return boolean value
- */
- public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name, Boolean defVal) {
- if (jsonObject != null && jsonObject.has(name)) {
- return jsonObject.get(name).getAsBoolean();
- } else {
- return false;
- }
- }
-
- /**
- *
- * @param name parameter name
- * @return boolean value
- */
- public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name) {
- return jsonObjectToBool(jsonObject, name, false);
- }
-
- /**
- *
- * @param name parameter name
- * @param defVal - default value;
- * @return integer value
- */
- public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name, Integer defVal) {
- if (jsonObject != null && jsonObject.has(name)) {
- return jsonObject.get(name).getAsInt();
- } else {
- return defVal;
- }
- }
-
- /**
- *
- * @param name parameter name
- * @return integer value
- */
- public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name) {
- return jsonObjectToInt(jsonObject, name, 0);
- }
-
- /**
- *
- * @param name parameter name
- * @param defVal - default value;
- * @return number value
- */
- public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name, Number defVal) {
- if (jsonObject != null && jsonObject.has(name)) {
- return jsonObject.get(name).getAsNumber();
- } else {
- return defVal;
- }
- }
-
- /**
- *
- * @param name parameter name
- * @return number value
- */
- public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name) {
- return jsonObjectToNumber(jsonObject, name, 0);
- }
-
- /************************************
- * TYPE UTILS
- ***********************************/
-
- /**
- * Return OnOffType from bool
- *
- * @param boolVal
- */
- public static OnOffType getOnOffType(@Nullable Boolean boolVal) {
- return boolVal != null ? OnOffType.from(boolVal) : OnOffType.OFF;
- }
-
- /**
- * Return OnOffType from bool
- *
- * @param intVal
- */
- public static OnOffType getOnOffType(Integer intVal) {
- return OnOffType.from(intVal != 0);
- }
-
- /**
- * Return StringType from String
- *
- * @param strVal
- */
- public static StringType getStringType(@Nullable String strVal) {
- return new StringType(strVal != null ? strVal : "");
- }
-
- /**
- * Return DecimalType from Double
- *
- * @param numVal
- */
- public static DecimalType getDecimalType(@Nullable Double numVal) {
- return new DecimalType((numVal != null ? numVal : 0));
- }
-
- /**
- * Return DecimalType from Integer
- *
- * @param numVal
- */
- public static DecimalType getDecimalType(@Nullable Integer numVal) {
- return new DecimalType((numVal != null ? numVal : 0));
- }
-
- /**
- * Return DecimalType from Long
- *
- * @param numVal
- */
- public static DecimalType getDecimalTypel(@Nullable Long numVal) {
- return new DecimalType((numVal != null ? numVal : 0));
- }
-
- /**
- *
- * @param numVal value 0-100
- * @return PercentType
- */
- public static PercentType getPercentType(@Nullable Integer numVal) {
- Integer val = limitVal(numVal, 0, 100);
- return new PercentType(val);
- }
-
- /**
- * Return HSBType from integers
- *
- * @param hue integer hue-color
- * @param saturation integer saturation
- * @param brightness integer brightness
- * @return HSBType
- */
- public static HSBType getHSBType(Integer hue, Integer saturation, Integer brightness) {
- DecimalType h = new DecimalType(hue);
- PercentType s = new PercentType(saturation);
- PercentType b = new PercentType(brightness);
- return new HSBType(h, s, b);
- }
-
- /**
- * Return QuantityType with Time
- *
- * @param numVal Number with value
- * @param unit TimeUnit ({@code Unit<Time>})
- * @return {@code QuantityType<Time>}
- */
- public static QuantityType<Time> getTimeType(@Nullable Number numVal, Unit<Time> unit) {
- return new QuantityType<>((numVal != null ? numVal : 0), unit);
- }
-
- /**
- * Return QuantityType with Power
- *
- * @param numVal Number with value
- * @param unit PowerUnit ({@code Unit<Power>})
- * @return {@code QuantityType<Power>}
- */
- public static QuantityType<Power> getPowerType(@Nullable Number numVal, Unit<Power> unit) {
- return new QuantityType<>((numVal != null ? numVal : 0), unit);
- }
-
- /**
- * Return QuantityType with Energy
- *
- * @param numVal Number with value
- * @param unit PowerUnit ({@code Unit<Power>})
- * @return {@code QuantityType<Energy>}
- */
- public static QuantityType<Energy> getEnergyType(@Nullable Number numVal, Unit<Energy> unit) {
- return new QuantityType<>((numVal != null ? numVal : 0), unit);
- }
-}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
+
+/**
+ * {@link ByteUtils} ByteUtils -
+ * Utility Helper Functions handling byte helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class ByteUtils {
+
+ /**
+ * Truncate Byte Array
+ *
+ * @param bytes full byteArray
+ * @param srcPos startindex
+ * @param newLength new size of byte array
+ * @return truncated byte array
+ */
+ public static byte[] truncateByteArray(byte[] bytes, int srcPos, int newLength) {
+ if (bytes.length < newLength) {
+ return bytes;
+ } else {
+ byte[] truncated = new byte[newLength];
+ System.arraycopy(bytes, srcPos, truncated, 0, newLength);
+ return truncated;
+ }
+ }
+
+ /**
+ * Concat Byte Arrays
+ *
+ * @param bytes bytes to concat as array
+ * @return byte array with concated input bytes
+ * @throws TapoErrorHandler
+ */
+ public static byte[] concatBytes(byte[]... bytes) throws TapoErrorHandler {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ try {
+ for (byte[] b : bytes) {
+ outputStream.write(b);
+ }
+ return outputStream.toByteArray();
+ } catch (Exception e) {
+ throw new TapoErrorHandler(ERR_DATA_TRANSORMATION);
+ }
+ }
+
+ /**
+ * Replace bytes in bytearray
+ *
+ * @param oldBytes original byte array
+ * @param replace replacing byte array
+ * @param startPos position bytes should be replaced
+ * @return array of bytes with replaced data
+ */
+ public static byte[] replaceBytes(byte[] oldBytes, byte[] replace, int startPos) {
+ System.arraycopy(replace, 0, oldBytes, startPos, replace.length);
+ return oldBytes;
+ }
+
+ /**
+ * HEX-STRING to byte convertion
+ *
+ * @param s hex-formated string
+ * @return array of bytes from hex-string
+ */
+ 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;
+ }
+
+ /**
+ * Convert byte to hex-string
+ *
+ * @param num single byte
+ * @return hex-string
+ */
+ public static String byteToHex(byte num) {
+ char[] hexDigits = new char[2];
+ hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
+ hexDigits[1] = Character.forDigit((num & 0xF), 16);
+ return new String(hexDigits);
+ }
+
+ /**
+ * Convert ByteArray to hex-string
+ *
+ * @param byteArray array of bytes to convert
+ * @return hex-string
+ */
+ public static String byteArrayToHex(byte[] byteArray) {
+ StringBuffer hexStringBuffer = new StringBuffer();
+ for (int i = 0; i < byteArray.length; i++) {
+ hexStringBuffer.append(byteToHex(byteArray[i]));
+ }
+ return hexStringBuffer.toString();
+ }
+
+ /**
+ * Convert byteArray to int
+ *
+ * @param bArr array of bytes
+ * @param byteOrder BigEndian or LittleEndian
+ * @return int value of bytebuffer
+ */
+ public static int byteArrayToInt(byte[] bArr, ByteOrder byteOrder) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+ byteBuffer.order(byteOrder);
+ byteBuffer.put(bArr);
+ return byteBuffer.getInt();
+ }
+
+ /**
+ * Convert byteArray to short
+ *
+ * @param bArr array of bytes
+ * @param byteOrder BigEndian or LittleEndian
+ * @return short value of bytebuffer
+ */
+ public static short byteArrayToShort(byte[] bArr, ByteOrder byteOrder) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+ byteBuffer.order(byteOrder);
+ byteBuffer.put(bArr);
+ return byteBuffer.getShort();
+ }
+
+ /**
+ * Convert int into byteArray
+ *
+ * @param i integer value
+ * @param byteOrder BigEndian or LittleEndian
+ * @return array of bytes
+ */
+ public static byte[] intToByteArray(int i, ByteOrder byteOrder) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(4);
+ byteBuffer.order(byteOrder);
+ byteBuffer.putInt(i);
+ return byteBuffer.array();
+ }
+
+ /**
+ * Convert short to byteArray
+ *
+ * @param s short value
+ * @param byteOrder BigEndian or LittleEndian
+ * @return array of bytes
+ */
+ public static byte[] shortToByteArray(short s, ByteOrder byteOrder) {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(2);
+ byteBuffer.order(byteOrder);
+ byteBuffer.putShort(s);
+ return byteBuffer.array();
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * {@link JsonUtils} JsonUtils -
+ * Utility Helper Functions handling json helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class JsonUtils {
+
+ /**
+ * Check if string is valid json
+ *
+ * @param json string to check
+ * @return true if is valid json
+ */
+ public static boolean isValidJson(String json) {
+ try {
+ JsonObject jsnObject = GSON.fromJson(json, JsonObject.class);
+ return jsnObject != null;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get String from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name (json-key)
+ * @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;
+ }
+ }
+
+ /**
+ * Get String from jsonObject
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name
+ * @return string value
+ */
+ public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name) {
+ return jsonObjectToString(jsonObject, name, "");
+ }
+
+ /**
+ * Get Boolean from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name (json-key)
+ * @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;
+ }
+ }
+
+ /**
+ * Get Boolean from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name (json-key)
+ * @return boolean value
+ */
+ public static Boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name) {
+ return jsonObjectToBool(jsonObject, name, false);
+ }
+
+ /**
+ * Get Integer from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name (json-key)
+ * @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;
+ }
+ }
+
+ /**
+ * Get Integer from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name (json-key)
+ * @return integer value
+ */
+ public static Integer jsonObjectToInt(@Nullable JsonObject jsonObject, String name) {
+ return jsonObjectToInt(jsonObject, name, 0);
+ }
+
+ /**
+ * Get Number from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @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;
+ }
+ }
+
+ /**
+ * Get Number from jsonObject with a json key
+ *
+ * @param jsonObject jsonObject
+ * @param name parameter name
+ * @return number value
+ */
+ public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name) {
+ return jsonObjectToNumber(jsonObject, name, 0);
+ }
+
+ /**
+ * Return class object from json formated string
+ *
+ * @param json json formatted string
+ * @param clazz class string should parsed to
+ */
+ public static <T> T getObjectFromJson(String json, Class<T> clazz) throws JsonParseException {
+ @Nullable
+ T result = GSON.fromJson(json, clazz);
+ if (result == null) {
+ throw new JsonParseException("result is null");
+ }
+ return result;
+ }
+
+ /**
+ * Return class object from JsonObject
+ *
+ * @param jso JsonOject
+ * @param clazz class string should parsed to
+ */
+ public static <T> T getObjectFromJson(JsonObject jso, Class<T> clazz) throws JsonParseException {
+ return getObjectFromJson(jso.toString(), clazz);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers.utils;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * {@link StringUtils} StringUtils -
+ * Utility Helper Functions handling String helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class StringUtils {
+ /**
+ * Return Boolean from string
+ *
+ * @param s - string to be converted
+ * @param defVal - Default Value
+ * @return boolean value
+ */
+ public static 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
+ * @return Integer
+ */
+ public static Integer stringToInteger(@Nullable String s, Integer defVal) {
+ if (s == null) {
+ return defVal;
+ }
+ try {
+ return Integer.valueOf(s);
+ } catch (Exception e) {
+ return defVal;
+ }
+ }
+
+ /**
+ * Get String from object
+ *
+ * @param o - object to be converted
+ * @param defVal - Default value
+ * @return String
+ */
+ public static String objectToString(@Nullable Object o, String defVal) {
+ if (o == null) {
+ return defVal;
+ }
+ try {
+ return o.toString();
+ } catch (Exception e) {
+ return defVal;
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
+import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tapocontrol.internal.discovery.dto.TapoDiscoveryResult;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * {@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;
+ }
+
+ /**
+ * compare tow values against an comparator and return the other one
+ * if both are null, comparator will be returned - if both have values val2 will be returned
+ *
+ * @param <T> Type of return value
+ * @param val1 fist value to campare - will be returned if val2 is null or matches comparator
+ * @param val2 second value to compare - will be returned if val1 is null or matches comparator
+ * @param comparator compared values with this
+ * @return
+ */
+ public static <T> T compareValuesAgainstComparator(@Nullable T val1, @Nullable T val2, T comparator) {
+ if (val1 == null && val2 == null) {
+ return comparator;
+ } else if (val1 != null && (val2 == null || val2.equals(comparator))) {
+ return Objects.requireNonNull(val1);
+ } else if (val1 == null || val1.equals(comparator)) {
+ return Objects.requireNonNull(val2);
+ } else {
+ return Objects.requireNonNull(val2);
+ }
+ }
+
+ /**
+ * Format MAC-Address replacing old division chars and add new one
+ *
+ * @param mac unformated mac-Address
+ * @param newDivisionChar new division char (e.g. ":","-" )
+ * @return new formated mac-Address
+ */
+ public static String formatMac(String mac, char newDivisionChar) {
+ String unformatedMac = unformatMac(mac);
+ return unformatedMac.replaceAll("(.{2})", "$1" + newDivisionChar).substring(0, 17);
+ }
+
+ /**
+ * unformat MAC-Address replace all division chars
+ *
+ * @param mac string with mac address
+ * @return mac address without any division chars
+ */
+ public static String unformatMac(String mac) {
+ mac = mac.replace("-", "");
+ mac = mac.replace(":", "");
+ mac = mac.replace(".", "");
+ return mac;
+ }
+
+ /**
+ * Get DeviceModel from String - Formats different spellings in model-strings
+ *
+ * @param device JsonObject with deviceData
+ * @return String with DeviceModel
+ */
+ public static String getDeviceModel(TapoDiscoveryResult device) {
+ return getDeviceModel(device.deviceModel());
+ }
+
+ /**
+ * Get DeviceModel from String - Formats different spellings in model-strings
+ *
+ * @param deviceModel String to find model from
+ * @return String with DeviceModel
+ */
+ public static String getDeviceModel(String deviceModel) {
+ try {
+ deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
+ deviceModel = deviceModel.replace("Tapo", "");
+ deviceModel = deviceModel.replace("Series", "");
+ deviceModel = deviceModel.trim();
+ deviceModel = deviceModel.replace(" ", "_");
+ deviceModel = deviceModel.substring(0, 4);
+ return deviceModel;
+ } catch (Exception e) {
+ return "";
+ }
+ }
+
+ /**
+ * GET DEVICE LABEL
+ *
+ * @param device JsonObject with deviceData
+ * @return String with DeviceLabel
+ */
+ public static String getDeviceLabel(TapoDiscoveryResult device) {
+ try {
+ String deviceLabel = "";
+ String deviceModel = getDeviceModel(device);
+ String alias = device.alias();
+ ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
+
+ if (SUPPORTED_HUB_UIDS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_HUB;
+ } else if (SUPPORTED_SOCKET_UIDS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_SOCKET;
+ } else if (SUPPORTED_SOCKET_STRIP_UIDS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_SOCKET_STRIP;
+ } 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;
+ } else if (SUPPORTED_LIGHT_STRIP_UIDS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_LIGHTSTRIP;
+ } else if (SUPPORTED_SMART_CONTACTS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_SMART_CONTACT;
+ } else if (SUPPORTED_MOTION_SENSORS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_MOTION_SENSOR;
+ } else if (SUPPORTED_WHEATHER_SENSORS.contains(deviceUID)) {
+ deviceLabel = DEVICE_DESCRIPTION_TEMP_SENSOR;
+ }
+ if (alias.length() > 0) {
+ return String.format("%s %s %s (%s)", DEVICE_VENDOR, deviceModel, deviceLabel, alias);
+ }
+ return String.format("%s %s %s", DEVICE_VENDOR, deviceModel, deviceLabel);
+ } catch (Exception e) {
+ return "";
+ }
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2024 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tapocontrol.internal.helpers.utils;
+
+import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+import javax.measure.quantity.Temperature;
+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;
+
+/**
+ * {@link TypeUtils} TypeUtils -
+ * Utility Helper Functions handling type helper functions
+ *
+ * @author Christian Wild - Initial Initial contribution
+ */
+@NonNullByDefault
+public class TypeUtils {
+ /**
+ * Return OnOffType from bool
+ *
+ * @param boolVal
+ */
+ public static OnOffType getOnOffType(@Nullable Boolean boolVal) {
+ return boolVal != null ? OnOffType.from(boolVal) : OnOffType.OFF;
+ }
+
+ /**
+ * Return OnOffType from Integer
+ *
+ * @param intVal
+ */
+ public static OnOffType getOnOffType(Integer intVal) {
+ return OnOffType.from(intVal != 0);
+ }
+
+ /**
+ * Return StringType from String
+ *
+ * @param strVal
+ */
+ public static StringType getStringType(@Nullable String strVal) {
+ return new StringType(strVal != null ? strVal : "");
+ }
+
+ /**
+ * Return DecimalType from Double
+ *
+ * @param numVal
+ */
+ public static DecimalType getDecimalType(@Nullable Double numVal) {
+ return new DecimalType((numVal != null ? numVal : 0));
+ }
+
+ /**
+ * Return DecimalType from Integer
+ *
+ * @param numVal
+ */
+ public static DecimalType getDecimalType(@Nullable Integer numVal) {
+ return new DecimalType((numVal != null ? numVal : 0));
+ }
+
+ /**
+ * Return DecimalType from Long
+ *
+ * @param numVal
+ */
+ public static DecimalType getDecimalTypel(@Nullable Long numVal) {
+ return new DecimalType((numVal != null ? numVal : 0));
+ }
+
+ /**
+ *
+ * @param numVal value 0-100
+ * @return PercentType
+ */
+ public static PercentType getPercentType(@Nullable Integer numVal) {
+ Integer val = limitVal(numVal, 0, 100);
+ return new PercentType(val);
+ }
+
+ /**
+ * Return HSBType from integers
+ *
+ * @param hue integer hue-color
+ * @param saturation integer saturation
+ * @param brightness integer brightness
+ * @return HSBType
+ */
+ public static HSBType getHSBType(Integer hue, Integer saturation, Integer brightness) {
+ DecimalType h = new DecimalType(hue);
+ PercentType s = new PercentType(saturation);
+ PercentType b = new PercentType(brightness);
+ return new HSBType(h, s, b);
+ }
+
+ /**
+ * Return QuantityType with Time
+ *
+ * @param numVal Number with value
+ * @param unit TimeUnit
+ * @return QuantityType of Type Time
+ */
+ public static QuantityType<Time> getTimeType(@Nullable Number numVal, Unit<Time> unit) {
+ return new QuantityType<>((numVal != null ? numVal : 0), unit);
+ }
+
+ /**
+ * Return QuantityType with Power
+ *
+ * @param numVal Number with value
+ * @param unit PowerUnit
+ * @return QuantityType of Type Power
+ */
+ public static QuantityType<Power> getPowerType(@Nullable Number numVal, Unit<Power> unit) {
+ return new QuantityType<>((numVal != null ? numVal : 0), unit);
+ }
+
+ /**
+ * Return QuantityType with Energy
+ *
+ * @param numVal Number with value
+ * @param unit PowerUnit
+ * @return QuantityType of Type Energy
+ */
+ public static QuantityType<Energy> getEnergyType(@Nullable Number numVal, Unit<Energy> unit) {
+ return new QuantityType<>((numVal != null ? numVal : 0), unit);
+ }
+
+ /**
+ * Return QuantityType with Temperature
+ *
+ * @param numVal Number with value
+ * @param unit TemperatureUnit
+ * @return QuantityType of Type Temperature
+ */
+ public static QuantityType<Temperature> getTemperatureType(@Nullable Number numVal, Unit<Temperature> unit) {
+ return new QuantityType<>((numVal != null ? numVal : 0), unit);
+ }
+}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
- *
- * @author Christian Wild - Initial contribution
- */
-
-@NonNullByDefault
-public final class TapoBridgeConfiguration {
- /* THING CONFIGUTATION PROPERTYS */
- public static final String CONFIG_EMAIL = "username";
- public static final String CONFIG_PASS = "password";
- 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 cloudDiscovery = false;
- public int reconnectInterval = CONFIG_CLOUD_FIXED_INTERVAL;
- public int discoveryInterval = 60;
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.MAC_DIVISION_CHAR;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.formatMac;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Tapo Child Device Information class
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class TapoChild {
- private String fwVer = "";
- private String hwVer = "";
- private String type = "";
- private String model = "";
- private String mac = "";
- private String category = "";
- private String deviceId = "";
- private boolean overheatStatus = false;
- private int bindCount = 0;
- private long onTime = 0;
- private int slotNumber = 0;
- private int position = 0;
- private String nickname = "";
- private boolean deviceOn = false;
- private String region = "";
-
- /***********************************
- *
- * GET VALUES
- *
- ************************************/
-
- public String getFirmwareVersion() {
- return fwVer;
- }
-
- public String getHardwareVersion() {
- return hwVer;
- }
-
- public Boolean isOff() {
- return !deviceOn;
- }
-
- public Boolean isOn() {
- return deviceOn;
- }
-
- public String getMAC() {
- return formatMac(mac, MAC_DIVISION_CHAR);
- }
-
- public String getModel() {
- return model.replace(" Series", "");
- }
-
- public String getNickname() {
- return nickname;
- }
-
- public Number getOnTime() {
- return onTime;
- }
-
- public String getRegion() {
- return region;
- }
-
- public String getRepresentationProperty() {
- return getMAC();
- }
-
- public String getSerial() {
- return deviceId;
- }
-
- public String getType() {
- return type;
- }
-
- public String getFwVer() {
- return fwVer;
- }
-
- public String getHwVer() {
- return hwVer;
- }
-
- public String getMac() {
- return mac;
- }
-
- public String getCategory() {
- return category;
- }
-
- public String getDeviceId() {
- return deviceId;
- }
-
- public Boolean getOverheatStatus() {
- return overheatStatus;
- }
-
- public Integer getBindCount() {
- return bindCount;
- }
-
- public Integer getSlotNumber() {
- return slotNumber;
- }
-
- public Integer getPosition() {
- return position;
- }
-
- public Boolean getDeviceOn() {
- return deviceOn;
- }
-
- public void setDeviceOn(Boolean deviceOn) {
- this.deviceOn = deviceOn;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Tapo-Child Structure Class
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class TapoChildData {
- private int startIndex = 0;
- private int sum = 0;
- private List<TapoChild> childDeviceList = List.of();
-
- public int getStartIndex() {
- return startIndex;
- }
-
- public int getSum() {
- return sum;
- }
-
- public List<TapoChild> getChildDeviceList() {
- return childDeviceList;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link TapoDeviceConfiguration} class contains fields mapping bridge configuration parameters.
- *
- * @author Christian Wild - Initial contribution
- */
-
-@NonNullByDefault
-public final class TapoDeviceConfiguration {
- /* THING CONFIGUTATION PROPERTYS */
- public static final String CONFIG_DEVICE_IP = "ipAddress";
- public static final String CONFIG_UPDATE_INTERVAL = "pollingInterval";
-
- /* thing configuration parameter. */
- public String ipAddress = "";
- public int pollingInterval = 30;
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.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, JSON_KEY_BRIGHTNESS);
- this.colorTemp = jsonObjectToInt(jsonObject, JSON_KEY_COLORTEMP, BULB_MIN_COLORTEMP);
- this.deviceId = jsonObjectToString(jsonObject, JSON_KEY_ID);
- this.deviceOn = jsonObjectToBool(jsonObject, JSON_KEY_ON);
- this.fwVer = jsonObjectToString(jsonObject, JSON_KEY_FW);
- this.hue = jsonObjectToInt(jsonObject, JSON_KEY_HUE);
- this.hwVer = jsonObjectToString(jsonObject, JSON_KEY_HW_VER);
- this.ip = jsonObjectToString(jsonObject, JSON_KEY_IP);
- this.mac = jsonObjectToString(jsonObject, JSON_KEY_MAC);
- this.model = jsonObjectToString(jsonObject, JSON_KEY_MODEL);
- this.nickname = jsonObjectToString(jsonObject, JSON_KEY_NICKNAME);
- this.onTime = jsonObjectToNumber(jsonObject, JSON_KEY_ONTIME);
- this.overheated = jsonObjectToBool(jsonObject, JSON_KEY_OVERHEAT);
- this.region = jsonObjectToString(jsonObject, JSON_KEY_REGION);
- this.saturation = jsonObjectToInt(jsonObject, JSON_KEY_SATURATION);
- this.signalLevel = jsonObjectToInt(jsonObject, JSON_KEY_SIGNAL_LEVEL);
- this.rssi = jsonObjectToInt(jsonObject, JSON_KEY_RSSI);
- this.timeUsagePast7 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_7);
- this.timeUsagePast30 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_30);
- this.timeUsageToday = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_TODAY);
- this.type = jsonObjectToString(jsonObject, JSON_KEY_TYPE);
-
- if (this.hasLightEffect()) {
- this.lightEffect = lightEffect.setData(jsonObject);
- }
- }
-
- /***********************************
- *
- * CHECK FOR CHILD TYPES
- *
- ************************************/
- public Boolean hasLightEffect() {
- return this.jsonObject.has(JSON_KEY_LIGHTNING_EFFECT) || this.jsonObject.has(JSON_KEY_LIGHTNING_DYNAMIC_ENABLE);
- }
-
- /***********************************
- *
- * 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(" Series", "");
- }
-
- public String getNickname() {
- return nickname;
- }
-
- public Number getOnTime() {
- return onTime;
- }
-
- public String getRegion() {
- return region;
- }
-
- public String getRepresentationProperty() {
- return getMAC();
- }
-
- public Integer getSaturation() {
- return saturation;
- }
-
- public String getSerial() {
- return deviceId;
- }
-
- public Integer getSignalLevel() {
- return signalLevel;
- }
-
- public Integer getRSSI() {
- return rssi;
- }
-
- public Number getTimeUsagePast7() {
- return timeUsagePast7;
- }
-
- public Number getTimeUsagePast30() {
- return timeUsagePast30;
- }
-
- public Number getTimeUsagePastToday() {
- return timeUsageToday;
- }
-
- public String getType() {
- return type;
- }
-
- @Override
- public String toString() {
- return jsonObject.toString();
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-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 com.google.gson.JsonObject;
-
-/**
- * Tapo-Energy-Monitor Structure Class
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoEnergyData {
- private Number currentPower = 0;
- private Number todayEnergy = 0;
- private Number monthEnergy = 0;
- private Number todayRuntime = 0;
- private Number monthRuntime = 0;
- private Number[] past24h = new Number[24];
- private Number[] past30d = new Number[30];
- private Number[] past1y = new Number[12];
-
- private JsonObject jsonObject = new JsonObject();
-
- /**
- * INIT
- */
- public TapoEnergyData() {
- setData();
- }
-
- /**
- * Init DeviceInfo with new Data;
- *
- * @param jso JsonObject new Data
- */
- public TapoEnergyData(JsonObject jso) {
- setData(jso);
- }
-
- /**
- * Set Data (new JsonObject)
- *
- * @param jso JsonObject new Data
- */
- public TapoEnergyData setData(JsonObject jso) {
- /* create empty jsonObject to set efault values if has no energydata */
- if (jso.has(JSON_KEY_ENERGY_POWER)) {
- this.jsonObject = jso;
- } else {
- this.jsonObject = new JsonObject();
- }
- setData();
- return this;
- }
-
- private void setData() {
- this.currentPower = (float) jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_POWER) / 1000;
-
- this.todayEnergy = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_ENERGY_TODAY);
- this.monthEnergy = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_ENERGY_MONTH);
- this.todayRuntime = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_RUNTIME_TODAY);
- this.monthRuntime = jsonObjectToInt(jsonObject, JSON_KEY_ENERGY_RUNTIME_MONTH);
- this.past24h = new Number[24];
- this.past30d = new Number[30];
- this.past1y = new Number[12];
- }
-
- /***********************************
- *
- * GET VALUES
- *
- ************************************/
-
- public Number getCurrentPower() {
- return currentPower;
- }
-
- public Number getTodayEnergy() {
- return todayEnergy;
- }
-
- public Number getMonthEnergy() {
- return monthEnergy;
- }
-
- public Number getYearEnergy() {
- int sum = 0;
- for (int i = 0; i < past1y.length; i++) {
- sum += past1y[i].intValue();
- }
- return sum;
- }
-
- public Number getTodayRuntime() {
- return todayRuntime;
- }
-
- public Number getMonthRuntime() {
- return monthRuntime;
- }
-
- public Number[] getPast24hUsage() {
- return past24h;
- }
-
- public Number[] getPast30dUsage() {
- return past30d;
- }
-
- public Number[] getPast1yUsage() {
- return past1y;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.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 Boolean enable = false;
- private String id = "";
- private String name = "";
- private Boolean custom = false;
- private Integer brightness = 0;
- private Integer[] colorTempRange = { 9000, 9000 }; // :[9000,9000]
- private Color[] displayColors = { Color.WHITE };
-
- private JsonObject jsonObject = new JsonObject();
-
- /**
- * INIT
- */
- public TapoLightEffect() {
- }
-
- /**
- * 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 (jso.has(JSON_KEY_LIGHTNING_EFFECT)) {
- this.jsonObject = jso.getAsJsonObject(JSON_KEY_LIGHTNING_EFFECT);
- this.enable = jsonObjectToBool(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ENABLE);
- this.id = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ID, JSON_KEY_LIGHTNING_EFFECT_OFF);
- this.name = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_NAME);
- this.custom = jsonObjectToBool(jsonObject, JSON_KEY_LIGHTNING_EFFECT_CUSTOM);
- this.brightness = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS);
- } else if (jso.has(JSON_KEY_LIGHTNING_DYNAMIC_ENABLE)) {
- this.jsonObject = jso;
- this.enable = jsonObjectToBool(jsonObject, JSON_KEY_LIGHTNING_DYNAMIC_ENABLE);
- this.id = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_DYNAMIC_ID, JSON_KEY_LIGHTNING_EFFECT_OFF);
- } else {
- setDefaults();
- }
- return this;
- }
-
- /**
- * Set default values
- */
- private void setDefaults() {
- this.jsonObject = new JsonObject();
- this.enable = false;
- this.id = JSON_KEY_LIGHTNING_EFFECT_OFF;
- this.name = "";
- this.custom = false;
- this.brightness = 100;
- }
-
- /***********************************
- *
- * SET VALUES
- *
- ************************************/
-
- public void setEnable(Boolean enable) {
- this.enable = enable;
- }
-
- public void setName(String value) {
- this.name = value;
- }
-
- public void setCustom(Boolean enable) {
- this.custom = enable;
- }
-
- public void setBrightness(Integer value) {
- this.brightness = value;
- }
-
- /***********************************
- *
- * GET VALUES
- *
- ************************************/
-
- public Boolean getEnable() {
- return enable;
- }
-
- public String getId() {
- return id;
- }
-
- public String getName() {
- return name;
- }
-
- public Boolean getCustom() {
- return custom;
- }
-
- public Integer getBrightness() {
- return brightness;
- }
-
- public Integer[] getColorTempRange() {
- return colorTempRange;
- }
-
- public Color[] getDisplayColors() {
- return displayColors;
- }
-
- @Override
- public String toString() {
- return jsonObject.toString();
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * {@link TapoSubRequest} holds data sent to device in order to act on a child
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public record TapoSubRequest(String method, Object params) {
- private record ChildRequest(String device_id, @SerializedName("requestData") TapoSubRequest requestData) {
- }
-
- private record SubMultiple(List<TapoSubRequest> requests) {
-
- private SubMultiple(String method, TapoChild params) {
- this(List.of(new TapoSubRequest(method, params)));
- }
- }
-
- public TapoSubRequest(String deviceId, String method, TapoChild params) {
- this(DEVICE_CMD_CONTROL_CHILD, new ChildRequest(deviceId,
- new TapoSubRequest(DEVICE_CMD_MULTIPLE_REQ, new SubMultiple(method, params))));
- }
-}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+ <config-description uri="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 to get all in Tapo-App registered devices. Includes DeviceName. IP-Address
+ and Encryption has to set manually if device is not found via udpDiscovery</description>
+ <default>false</default>
+ <advanced>false</advanced>
+ </parameter>
+ <parameter name="udpDiscovery" type="boolean" required="false">
+ <label>UDP Discovery</label>
+ <description>Use UDP Discovery-Service to discover online devices in the local network. Includes Encryption and
+ IP-Address</description>
+ <default>false</default>
+ <advanced>false</advanced>
+ </parameter>
+ <parameter name="onlyLocalOnlineDevices" type="boolean" required="false">
+ <label>Discover only online devices</label>
+ <description>Uses Cloud and UPD-Discovery to get more informations but will only discover online devices via UDP</description>
+ <default>false</default>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="broadcastAddress" type="text" required="false">
+ <context>network-address</context>
+ <label>Broadcast Address</label>
+ <description>Broadcast-Address UDP-requests are sent to. Change it to the broadcast-address of your ip-range (e.g.
+ '192.168.1.255') if you have problems with
+ default (255.255.255.255)</description>
+ <default>255.255.255.255</default>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="discoveryInterval" type="integer" min="1" max="10080" required="false">
+ <label>Background Discovery Interval</label>
+ <description>Interval background discovery in minutes (default 60)</description>
+ <default>60</default>
+ <advanced>true</advanced>
+ </parameter>
+ </config-description>
+</config-description:config-descriptions>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<config-description:config-descriptions
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
- xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
-
- <config-description uri="thing-type:tapo:device">
- <parameter name="ipAddress" type="text" required="true">
- <context>network-address</context>
- <label>IP Address</label>
- </parameter>
- <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
- <label>Refresh Interval</label>
- <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
- <default>30</default>
- <advanced>true</advanced>
- </parameter>
- </config-description>
-
- <config-description uri="bridge-type:tapo:bridge">
- <parameter name="username" type="text" required="true">
- <context>email</context>
- <label>Username</label>
- <description>Tapo-Cloud Login User (e-Mail)</description>
- </parameter>
- <parameter name="password" type="text" required="true">
- <context>password</context>
- <label>Password</label>
- <description>Tapo-Cloud Login Password</description>
- </parameter>
- <parameter name="cloudDiscovery" type="boolean" required="false">
- <label>Cloud Discovery</label>
- <description>Use Cloud Discovery-Service</description>
- <default>false</default>
- <advanced>false</advanced>
- </parameter>
- <parameter name="discoveryInterval" type="integer" min="1" max="10080" required="false">
- <label>Background Discovery Interval</label>
- <description>Interval background discovery in minutes (default 60)</description>
- <default>60</default>
- <advanced>true</advanced>
- </parameter>
- </config-description>
-</config-description:config-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<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="httpPort" type="integer" required="true">
+ <label>Port</label>
+ <description>HTTP-Communication Port (default 80)</description>
+ <default>80</default>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="protocol" type="text" required="true">
+ <label>Protocol</label>
+ <description>Communication Protocol</description>
+ <options>
+ <option value="">Direct HTTP</option>
+ <option value="AES">Secured HTTP Protocol</option>
+ <option value="KLAP">Secured KLAP HTTP Protocol</option>
+ </options>
+ <limitToOptions>true</limitToOptions>
+ <default>AES</default>
+ <advanced>true</advanced>
+ </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). Set it below 10 seconds may cause
+ communication issues (not recommed).</description>
+ <default>30</default>
+ <advanced>true</advanced>
+ </parameter>
+ </config-description>
+</config-description:config-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+ <config-description uri="bridge-type:tapo:hub">
+ <parameter name="ipAddress" type="text" required="true">
+ <context>network-address</context>
+ <label>IP Address</label>
+ </parameter>
+ <parameter name="httpPort" type="integer" required="true">
+ <label>Port</label>
+ <description>HTTP-Communication Port (default 80)</description>
+ <default>80</default>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="protocol" type="text" required="true">
+ <label>Protocol</label>
+ <description>Communication Protocol</description>
+ <options>
+ <option value="">Direct HTTP</option>
+ <option value="AES">Secured HTTP Protocol</option>
+ <option value="KLAP">Secured KLAP HTTP Protocol</option>
+ </options>
+ <limitToOptions>true</limitToOptions>
+ <default>AES</default>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
+ <label>Refresh Interval</label>
+ <description>Refresh interval for refreshing hub informations in seconds (0=disabled). Set it below 10 seconds may
+ cause communication issues (not recommed).</description>
+ <default>10</default>
+ <advanced>true</advanced>
+ </parameter>
+ <parameter name="backgroundDiscovery" type="boolean" required="false">
+ <label>Background Discovery</label>
+ <description>If background discovery is enabled, devices will be discovered after every polling request</description>
+ <default>false</default>
+ <advanced>true</advanced>
+ </parameter>
+ </config-description>
+
+</config-description:config-descriptions>
# thing types
+thing-type.tapocontrol.H100.label = Tapo Hub H100
+thing-type.tapocontrol.H100.description = Tapo SmartHub H100
thing-type.tapocontrol.L510.label = L510 Series White-Bulb
thing-type.tapocontrol.L510.description = Tapo Smart dimmable White-Light-Bulb
thing-type.tapocontrol.L530.label = L530 Series Color-Bulb
thing-type.tapocontrol.P115.description = Tapo Smart Monitoring Wifi Plug
thing-type.tapocontrol.P300.label = P300 Power Strip
thing-type.tapocontrol.P300.description = Tapo Smart Wi-Fi Power Strip
+thing-type.tapocontrol.T110.label = T110 Smart Contact Sensor
+thing-type.tapocontrol.T110.description = Tapo Smart Window/Door Sensor
+thing-type.tapocontrol.T310.label = T310 Temperature Sensor
+thing-type.tapocontrol.T310.description = Tapo Smart Temperature and Humidity Sensor
+thing-type.tapocontrol.T315.label = T315 Temperature Monitor
+thing-type.tapocontrol.T315.description = Tapo Smart Temperature and Humidity Monitor
thing-type.tapocontrol.bridge.label = Cloud-Login
thing-type.tapocontrol.bridge.description = Cloud Connector. Acts as device-bridge
# thing types config
+bridge-type.config.tapo.bridge.broadcastAddress.label = Broadcast Address
+bridge-type.config.tapo.bridge.broadcastAddress.description = Broadcast-Address UDP-requests are sent to. Change it to the broadcast-address of your ip-range (e.g. '192.168.1.255') if you have problems with default (255.255.255.255)
bridge-type.config.tapo.bridge.cloudDiscovery.label = Cloud Discovery
-bridge-type.config.tapo.bridge.cloudDiscovery.description = Use Cloud Discovery-Service
+bridge-type.config.tapo.bridge.cloudDiscovery.description = Use Cloud Discovery-Service to get all in Tapo-App registered devices. Includes DeviceName. IP-Address and Encryption has to set manually if device is not found via udpDiscovery
bridge-type.config.tapo.bridge.discoveryInterval.label = Background Discovery Interval
bridge-type.config.tapo.bridge.discoveryInterval.description = Interval background discovery in minutes (default 60)
+bridge-type.config.tapo.bridge.onlyLocalOnlineDevices.label = Discover only online devices
+bridge-type.config.tapo.bridge.onlyLocalOnlineDevices.description = Uses Cloud and UPD-Discovery to get more informations but will only discover online devices via UDP
bridge-type.config.tapo.bridge.password.label = Password
bridge-type.config.tapo.bridge.password.description = Tapo-Cloud Login Password
+bridge-type.config.tapo.bridge.udpDiscovery.label = UDP Discovery
+bridge-type.config.tapo.bridge.udpDiscovery.description = Use UDP Discovery-Service to discover online devices in the local network. Includes Encryption and IP-Address
bridge-type.config.tapo.bridge.username.label = Username
bridge-type.config.tapo.bridge.username.description = Tapo-Cloud Login User (e-Mail)
+bridge-type.config.tapo.hub.backgroundDiscovery.label = Background Discovery
+bridge-type.config.tapo.hub.backgroundDiscovery.description = If background discovery is enabled, devices will be discovered after every polling request
+bridge-type.config.tapo.hub.httpPort.label = Port
+bridge-type.config.tapo.hub.httpPort.description = HTTP-Communication Port (default 80)
+bridge-type.config.tapo.hub.ipAddress.label = IP Address
+bridge-type.config.tapo.hub.pollingInterval.label = Refresh Interval
+bridge-type.config.tapo.hub.pollingInterval.description = Refresh interval for refreshing hub informations in seconds (0=disabled). Set it below 10 seconds may cause communication issues (not recommed).
+bridge-type.config.tapo.hub.protocol.label = Protocol
+bridge-type.config.tapo.hub.protocol.description = Communication Protocol
+bridge-type.config.tapo.hub.protocol.option. = Direct HTTP
+bridge-type.config.tapo.hub.protocol.option.AES = Secured HTTP Protocol
+bridge-type.config.tapo.hub.protocol.option.KLAP = Secured KLAP HTTP Protocol
+thing-type.config.tapo.device.httpPort.label = Port
+thing-type.config.tapo.device.httpPort.description = HTTP-Communication Port (default 80)
thing-type.config.tapo.device.ipAddress.label = IP Address
thing-type.config.tapo.device.pollingInterval.label = Refresh Interval
-thing-type.config.tapo.device.pollingInterval.description = Refresh interval for refreshing the data in seconds. (0=disabled)
+thing-type.config.tapo.device.pollingInterval.description = Refresh interval for refreshing the data in seconds (0=disabled). Set it below 10 seconds may cause communication issues (not recommed).
+thing-type.config.tapo.device.protocol.label = Protocol
+thing-type.config.tapo.device.protocol.description = Communication Protocol
+thing-type.config.tapo.device.protocol.option. = Direct HTTP
+thing-type.config.tapo.device.protocol.option.AES = Secured HTTP Protocol
+thing-type.config.tapo.device.protocol.option.KLAP = Secured KLAP HTTP Protocol
# channel group types
+channel-group-type.tapocontrol.childDeviceState.label = Device State
+channel-group-type.tapocontrol.childDeviceState.description = Information about the device
channel-group-type.tapocontrol.colorBulb.label = Color Light Bulb
channel-group-type.tapocontrol.colorBulb.description = Tapo Multicolor Smart Light Bulb
+channel-group-type.tapocontrol.contactSensor.label = Contact Sensor
+channel-group-type.tapocontrol.contactSensor.description = Door/Window Contact Sensor Channels
channel-group-type.tapocontrol.deviceState.label = Device State
channel-group-type.tapocontrol.deviceState.description = Information about the device
channel-group-type.tapocontrol.deviceStateS.label = Device State
channel-group-type.tapocontrol.lightBulb.description = Tapo Smart Light Bulb
channel-group-type.tapocontrol.lightEffectL530.label = Lightning Effect
channel-group-type.tapocontrol.lightEffectL530.description = Tapo Lightning Effects
+channel-group-type.tapocontrol.lightEffectL900.label = Lightning Effect
+channel-group-type.tapocontrol.lightEffectL900.description = Tapo Lightning Effects
channel-group-type.tapocontrol.lightEffectL920.label = Lightning Effect
channel-group-type.tapocontrol.lightEffectL920.description = Tapo Lightning Effects
-channel-group-type.tapocontrol.lightEffectL920.label = Lightning Effect
-channel-group-type.tapocontrol.lightEffectL920.description = Tapo Lightning Effects
+channel-group-type.tapocontrol.lightEffectL930.label = Lightning Effect
+channel-group-type.tapocontrol.lightEffectL930.description = Tapo Lightning Effects
channel-group-type.tapocontrol.lightStrip.label = Color Light Strip
channel-group-type.tapocontrol.lightStrip.description = Tapo Multicolor Smart Light Strip
channel-group-type.tapocontrol.powerStrip.label = SmartPlug
channel-group-type.tapocontrol.powerStrip.channel.output2.description = Switches the power state on/off of the second socket
channel-group-type.tapocontrol.powerStrip.channel.output3.label = Output Switch 3
channel-group-type.tapocontrol.powerStrip.channel.output3.description = Switches the power state on/off of the third socket
+channel-group-type.tapocontrol.smartHubAlarms.label = Smart Hub Alarms
+channel-group-type.tapocontrol.smartHubAlarms.description = Tapo Smart Hub Alarms
channel-group-type.tapocontrol.smartPlug.label = SmartPlug
channel-group-type.tapocontrol.smartPlug.description = Tapo Smart Plug Power Outlet
+channel-group-type.tapocontrol.temperatureSensor.label = Wheater Sensor
+channel-group-type.tapocontrol.temperatureSensor.description = Temperature and Humidity Sensor Channels
# channel types
channel-type.tapocontrol.actualPowerChannel.label = Power
channel-type.tapocontrol.actualPowerChannel.description = Actual power usage
+channel-type.tapocontrol.alarmIsActiveChannel.label = Active Alarm
+channel-type.tapocontrol.alarmIsActiveChannel.description = Device has an active alarm
+channel-type.tapocontrol.alarmSourceChannel.label = Alarm Source
+channel-type.tapocontrol.alarmSourceChannel.description = Source of active alarm
+channel-type.tapocontrol.batteryLowChannel.label = Battery Low
+channel-type.tapocontrol.batteryLowChannel.description = Battery of device is low
+channel-type.tapocontrol.batteryLowEvent.label = Battery Low
+channel-type.tapocontrol.batteryLowEvent.description = Battery state has changed to low. Replace Battery
+channel-type.tapocontrol.colorBulbMode.label = Mode
+channel-type.tapocontrol.colorBulbMode.description = Working mode of device
+channel-type.tapocontrol.colorBulbMode.command.option.WHITE_LIGHT = White
+channel-type.tapocontrol.colorBulbMode.command.option.COLOR_LIGHT = Color
+channel-type.tapocontrol.colorBulbMode.command.option.LIGHT_FX = Effects
channel-type.tapocontrol.colorChannel.label = Color
channel-type.tapocontrol.colorChannel.description = Color
channel-type.tapocontrol.colorTemperature.label = Color Temperature
-channel-type.tapocontrol.colorTemperature.description = This channel supports adjusting the color temperature from 2700K to 6500K.
+channel-type.tapocontrol.colorTemperature.description = This channel supports adjusting the color temperature from 2200K to 6500K.
+channel-type.tapocontrol.contactCloseEvent.label = Contact Closed
+channel-type.tapocontrol.contactCloseEvent.description = Event is fired if contact changes from open to closed
+channel-type.tapocontrol.contactOpenEvent.label = Contact Opened
+channel-type.tapocontrol.contactOpenEvent.description = Event is fired if contact changes from closed to open
channel-type.tapocontrol.dimmerChannel.label = Brightness
channel-type.tapocontrol.dimmerChannel.description = Brightness
channel-type.tapocontrol.fade.label = Fade Light
channel-type.tapocontrol.fade.description = Make the light darker or lighter slowly
+channel-type.tapocontrol.humidityChannel.label = Humidity
+channel-type.tapocontrol.humidityChannel.description = Current relative humidity
+channel-type.tapocontrol.isOnlineChannel.label = Is Online
+channel-type.tapocontrol.isOnlineChannel.description = Device is Online
+channel-type.tapocontrol.isOpenChannel.label = Is Open
+channel-type.tapocontrol.isOpenChannel.description = Contact (Window/Door) is Open
channel-type.tapocontrol.l530fxList.label = Light Effect Theme
channel-type.tapocontrol.l530fxList.description = Name of active lightning effect
-channel-type.tapocontrol.l530fxList.state.option.off = None (No FX)
-channel-type.tapocontrol.l530fxList.state.option.custom = Custom
-channel-type.tapocontrol.l530fxList.state.option.L1 = Party
-channel-type.tapocontrol.l530fxList.state.option.L2 = Relax
+channel-type.tapocontrol.l530fxList.command.option.off = None (No FX)
+channel-type.tapocontrol.l530fxList.command.option.custom = Custom
+channel-type.tapocontrol.l530fxList.command.option.L1 = Party
+channel-type.tapocontrol.l530fxList.command.option.L2 = Relax
+channel-type.tapocontrol.l900fxList.label = Light Effect Theme
+channel-type.tapocontrol.l900fxList.description = Name of lightning effect
+channel-type.tapocontrol.l900fxList.state.option.off = None (No FX)
+channel-type.tapocontrol.l900fxList.state.option.aurora = Aurora
+channel-type.tapocontrol.l900fxList.state.option.bubbling_calderon = Bubbling Calderon
+channel-type.tapocontrol.l900fxList.state.option.christmas = Christmas
+channel-type.tapocontrol.l900fxList.state.option.christmas_light = Christmas Light
+channel-type.tapocontrol.l900fxList.state.option.candy_cane = Candy
+channel-type.tapocontrol.l900fxList.state.option.flicker = Flicker
+channel-type.tapocontrol.l900fxList.state.option.hanukkah = Hanukkah
+channel-type.tapocontrol.l900fxList.state.option.haunted_mansion = Haunted Mansion
+channel-type.tapocontrol.l900fxList.state.option.icicle = Icicle
+channel-type.tapocontrol.l900fxList.state.option.lightning = Lightning
+channel-type.tapocontrol.l900fxList.state.option.ocean = Ocean
+channel-type.tapocontrol.l900fxList.state.option.rainbow = Rainbow
+channel-type.tapocontrol.l900fxList.state.option.raindrop = Raindrop
+channel-type.tapocontrol.l900fxList.state.option.spring = Spring
+channel-type.tapocontrol.l900fxList.state.option.sunrise = Sunrise
+channel-type.tapocontrol.l900fxList.state.option.sunset = Sunset
+channel-type.tapocontrol.l900fxList.state.option.valentines = Valentines
channel-type.tapocontrol.l920fxList.label = Light Effect Theme
channel-type.tapocontrol.l920fxList.description = Name of lightning effect
-channel-type.tapocontrol.l920fxList.state.option. = None (No FX)
+channel-type.tapocontrol.l920fxList.state.option.off = None (No FX)
channel-type.tapocontrol.l920fxList.state.option.aurora = Aurora
channel-type.tapocontrol.l920fxList.state.option.bubbling_calderon = Bubbling Calderon
channel-type.tapocontrol.l920fxList.state.option.christmas = Christmas
channel-type.tapocontrol.l920fxList.state.option.valentines = Valentines
channel-type.tapocontrol.l930fxList.label = Light Effect Theme
channel-type.tapocontrol.l930fxList.description = Name of lightning effect
-channel-type.tapocontrol.l930fxList.state.option. = None (No FX)
+channel-type.tapocontrol.l930fxList.state.option.off = None (No FX)
channel-type.tapocontrol.l930fxList.state.option.aurora = Aurora
channel-type.tapocontrol.l930fxList.state.option.bubbling_calderon = Bubbling Calderon
channel-type.tapocontrol.l930fxList.state.option.christmas = Christmas
channel-type.tapocontrol.led.description = Switch the Smart Home device led on or off.
channel-type.tapocontrol.lightOn.label = Light On
channel-type.tapocontrol.lightOn.description = Switches the light on/off
+channel-type.tapocontrol.monthEnergyUsageChannel.label = Month Usage
+channel-type.tapocontrol.monthEnergyUsageChannel.description = Energy usage last month
+channel-type.tapocontrol.monthRuntimeChannel.label = Month Runtime
+channel-type.tapocontrol.monthRuntimeChannel.description = Runtime last month (On-Time)
channel-type.tapocontrol.ontime.label = On-Time
channel-type.tapocontrol.ontime.description = Number of seconds since the device was powered on
channel-type.tapocontrol.outputChannel.label = Output Switch
channel-type.tapocontrol.outputChannel.description = Switches the power state on/off
channel-type.tapocontrol.overheated.label = Device Overheated
channel-type.tapocontrol.overheated.description = ON if device is overheated
+channel-type.tapocontrol.temperatureChannel.label = Temperature
+channel-type.tapocontrol.temperatureChannel.description = Temperature as measured by the sensor
channel-type.tapocontrol.todayEnergyUsageChannel.label = Today Usage
channel-type.tapocontrol.todayEnergyUsageChannel.description = Today energy usage
channel-type.tapocontrol.todayRuntimeChannel.label = Today Runtime
channel-type.tapocontrol.todayRuntimeChannel.description = Today runtime (On-Time)
+# channel types
+
+channel-type.tapocontrol.l530fxList.state.option.off = None (No FX)
+channel-type.tapocontrol.l530fxList.state.option.custom = Custom
+channel-type.tapocontrol.l530fxList.state.option.L1 = Party
+channel-type.tapocontrol.l530fxList.state.option.L2 = Relax
+
+# thing types config
+
+bridge-type.config.tapo.hub.protocol.option.passthrough = Direct HTTP
+bridge-type.config.tapo.hub.protocol.option.securePassthrough = Secured HTTP Protocol
+bridge-type.config.tapo.hub.protocol.option.klap = Secured KLAP HTTP Protocol
+
+# thing types config
+
+thing-type.config.tapo.device.protocol.option.passthrough = Direct HTTP
+thing-type.config.tapo.device.protocol.option.securePassthrough = Secured HTTP Protocol
+thing-type.config.tapo.device.protocol.option.klap = Secured KLAP HTTP Protocol
+
+# channel types
+
+channel-type.tapocontrol.l920fxList.state.option. = None (No FX)
+channel-type.tapocontrol.l930fxList.state.option. = None (No FX)
+
# channel group types
channel-group-type.tapocontrol.lightEffect.label = Lightning Effect
# error messages
+error-api-unknown-com-error = recieived unknown com error(9999)
error-api-account = received account error (-2101)
error-api-aes-decode-fail = aes decode failed (-1005)
error-api-antitheft-conflict = device antitheft conflict (-2002)
error-api-multi-request-failed = multi request failed (1200)
error-api-null-transport = null transport error (1000)
error-api-params = received invalid parameter (-1008)
+error-api-protocol = encryption protocol error (1003)
error-api-quick-setup = quick setup error (-1201)
error-api-request-len-error = request length error (-1006)
error-api-request = invalid request or command (1002)
error-binding-connect-timeout = connection timeout - device not reachable (9010)
error-binding-cookie = cookie error (9002)
error-binding-credentials = invalid request or credentials (9003)
+error-binding-login = login failed (9004)
error-binding-device-offline = device offline (9009)
error-binding-http-response = invalid http-response (9001)
error-cloud-api-rate = api rate limit exceeded (-20004)
error-config-credentials = credentials not set (bridge) (10002)
error-config-ip = ip-address not valid (10001)
error-config-no-bridge = no bridge configured (10003)
+error-config-protocol = unknown protocol (10004)
error-unknown = unknown api error ({0})
+error-data-encrypting = encrypting failed (9500)
+error-data-decrypting = decrypting failed (9501)
+error-data-format = unexpected data format (9505)
+error-data-transormation = data transformation failed (9506)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <bridge-type id="H100">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="bridge"/>
+ </supported-bridge-type-refs>
+
+ <label>Tapo Hub H100</label>
+ <description>Tapo SmartHub H100</description>
+
+ <channel-groups>
+ <channel-group id="alarm" typeId="smartHubAlarms"/>
+ <channel-group id="device" typeId="deviceStateS"/>
+ </channel-groups>
+
+ <representation-property>macAddress</representation-property>
+
+ <config-description-ref uri="bridge-type:tapo:hub"/>
+ </bridge-type>
+</thing:thing-descriptions>
<item-type>String</item-type>
<label>Light Effect Theme</label>
<description>Name of active lightning effect</description>
- <state readOnly="false">
+ <state readOnly="false"/>
+ <command>
<options>
<option value="off">None (No FX)</option>
<option value="custom">Custom</option>
<option value="L1">Party</option>
<option value="L2">Relax</option>
</options>
- </state>
+ </command>
</channel-type>
</thing:thing-descriptions>
<description>Tapo Smart LED-Lightstrip</description>
<channel-groups>
<channel-group id="actuator" typeId="lightStrip"/>
+ <channel-group id="effects" typeId="lightEffectL900"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:tapo:device"/>
</thing-type>
+
+ <!-- Lightning Effect -->
+ <channel-group-type id="lightEffectL900">
+ <label>Lightning Effect</label>
+ <description>Tapo Lightning Effects</description>
+ <channels>
+ <channel id="fxName" typeId="l900fxList"/>
+ </channels>
+ </channel-group-type>
+
+ <!-- effect name -->
+ <channel-type id="l900fxList">
+ <item-type>String</item-type>
+ <label>Light Effect Theme</label>
+ <description>Name of lightning effect</description>
+ <state readOnly="false">
+ <options>
+ <option value="off">None (No FX)</option>
+ <option value="aurora">Aurora</option>
+ <option value="bubbling_calderon">Bubbling Calderon</option>
+ <option value="christmas">Christmas</option>
+ <option value="christmas_light">Christmas Light</option>
+ <option value="candy_cane">Candy</option>
+ <option value="flicker">Flicker</option>
+ <option value="hanukkah">Hanukkah</option>
+ <option value="haunted_mansion">Haunted Mansion</option>
+ <option value="icicle">Icicle</option>
+ <option value="lightning">Lightning</option>
+ <option value="ocean">Ocean</option>
+ <option value="rainbow">Rainbow</option>
+ <option value="raindrop">Raindrop</option>
+ <option value="spring">Spring</option>
+ <option value="sunrise">Sunrise</option>
+ <option value="sunset">Sunset</option>
+ <option value="valentines">Valentines</option>
+ </options>
+ </state>
+ </channel-type>
</thing:thing-descriptions>
<description>Tapo Smart Multicolor LED-Lightstrip</description>
<channel-groups>
<channel-group id="actuator" typeId="lightStrip"/>
+ <channel-group id="effects" typeId="lightEffectL920"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<label>Lightning Effect</label>
<description>Tapo Lightning Effects</description>
<channels>
- <channel id="fxBrightness" typeId="dimmerChannel"/>
<channel id="fxName" typeId="l920fxList"/>
- <channel id="fxColor1" typeId="colorChannel"/>
- <channel id="fxColor2" typeId="colorChannel"/>
- <channel id="fxColor3" typeId="colorChannel"/>
- <channel id="fxColor4" typeId="colorChannel"/>
</channels>
</channel-group-type>
<description>Name of lightning effect</description>
<state readOnly="false">
<options>
- <option value="">None (No FX)</option>
+ <option value="off">None (No FX)</option>
<option value="aurora">Aurora</option>
<option value="bubbling_calderon">Bubbling Calderon</option>
<option value="christmas">Christmas</option>
<description>Tapo Smart Multicolor LED-Lightstrip with ZoneControl</description>
<channel-groups>
<channel-group id="actuator" typeId="lightStrip"/>
+ <channel-group id="effects" typeId="lightEffectL930"/>
<channel-group id="device" typeId="deviceState"/>
</channel-groups>
<representation-property>macAddress</representation-property>
</thing-type>
<!-- Lightning Effect -->
- <channel-group-type id="lightEffectL920">
+ <channel-group-type id="lightEffectL930">
<label>Lightning Effect</label>
<description>Tapo Lightning Effects</description>
<channels>
- <channel id="fxBrightness" typeId="dimmerChannel"/>
<channel id="fxName" typeId="l930fxList"/>
- <channel id="fxColor1" typeId="colorChannel"/>
- <channel id="fxColor2" typeId="colorChannel"/>
- <channel id="fxColor3" typeId="colorChannel"/>
- <channel id="fxColor4" typeId="colorChannel"/>
</channels>
</channel-group-type>
<description>Name of lightning effect</description>
<state readOnly="false">
<options>
- <option value="">None (No FX)</option>
+ <option value="off">None (No FX)</option>
<option value="aurora">Aurora</option>
<option value="bubbling_calderon">Bubbling Calderon</option>
<option value="christmas">Christmas</option>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <!-- T110 THING-TYPE (SMART CONTACT SENSOR) -->
+ <thing-type id="T110">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="H100"/>
+ </supported-bridge-type-refs>
+
+ <label>T110 Smart Contact Sensor</label>
+ <description>Tapo Smart Window/Door Sensor</description>
+ <channel-groups>
+ <channel-group id="sensor" typeId="contactSensor"/>
+ <channel-group id="device" typeId="childDeviceState"/>
+ </channel-groups>
+ <representation-property>serialNumber</representation-property>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <!-- T310 THING-TYPE (SMART CONTACT SENSOR) -->
+ <thing-type id="T310">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="H100"/>
+ </supported-bridge-type-refs>
+
+ <label>T310 Temperature Sensor</label>
+ <description>Tapo Smart Temperature and Humidity Sensor</description>
+
+ <channel-groups>
+ <channel-group id="sensor" typeId="temperatureSensor"/>
+ <channel-group id="device" typeId="childDeviceState"/>
+ </channel-groups>
+ <representation-property>serialNumber</representation-property>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <!-- T315 THING-TYPE (SMART CONTACT SENSOR) -->
+ <thing-type id="T315">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="H100"/>
+ </supported-bridge-type-refs>
+
+ <label>T315 Temperature Monitor</label>
+ <description>Tapo Smart Temperature and Humidity Monitor</description>
+
+ <channel-groups>
+ <channel-group id="sensor" typeId="temperatureSensor"/>
+ <channel-group id="device" typeId="childDeviceState"/>
+ </channel-groups>
+ <representation-property>serialNumber</representation-property>
+ </thing-type>
+</thing:thing-descriptions>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="tapocontrol"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <!-- ############################### CHANNEL-GROUPS ############################### -->
+
+ <!-- CHANNEL GROUP TYPES -->
+ <!-- Device-Status 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>
+
+ <!-- Device-Status Channel Type (Small) -->
+ <channel-group-type id="deviceStateS">
+ <label>Device State</label>
+ <description>Information about the device</description>
+ <channels>
+ <channel id="wifiSignal" typeId="system.signal-strength"/>
+ <channel id="overheated" typeId="overheated"/>
+ </channels>
+ </channel-group-type>
+
+ <!-- Child-Device-Status Channel Type -->
+ <channel-group-type id="childDeviceState">
+ <label>Device State</label>
+ <description>Information about the device</description>
+ <channels>
+ <channel id="signalStrength" typeId="system.signal-strength"/>
+ <channel id="batteryLow" typeId="batteryLowChannel"/>
+ <channel id="batteryIsLow" typeId="batteryLowEvent"></channel>
+ </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>
+
+ <channel-group-type id="powerStrip">
+ <label>SmartPlug</label>
+ <description>Tapo Smart Plug Power Outlet</description>
+ <channels>
+ <channel id="output1" typeId="outputChannel">
+ <label>Output Switch 1</label>
+ <description>Switches the power state on/off of the first socket</description>
+ </channel>
+ <channel id="output2" typeId="outputChannel">
+ <label>Output Switch 2</label>
+ <description>Switches the power state on/off of the second socket</description>
+ </channel>
+ <channel id="output3" typeId="outputChannel">
+ <label>Output Switch 3</label>
+ <description>Switches the power state on/off of the third socket</description>
+ </channel>
+ </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"/>
+ </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="mode" typeId="colorBulbMode"/>
+ <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"/>
+ </channels>
+ </channel-group-type>
+
+ <!-- Energy Monitor -->
+ <channel-group-type id="energyMonitor">
+ <label>Energy Usage</label>
+ <description>Energy and Power usage</description>
+ <channels>
+ <channel id="actualPower" typeId="actualPowerChannel"></channel>
+ <channel id="todayEnergyUsage" typeId="todayEnergyUsageChannel"></channel>
+ <channel id="todayRuntime" typeId="todayRuntimeChannel"></channel>
+ <channel id="monthEnergyUsage" typeId="monthEnergyUsageChannel"></channel>
+ <channel id="monthRuntime" typeId="monthRuntimeChannel"></channel>
+ </channels>
+ </channel-group-type>
+
+ <!-- Smart Hub -->
+ <channel-group-type id="smartHubAlarms">
+ <label>Smart Hub Alarms</label>
+ <description>Tapo Smart Hub Alarms</description>
+ <channels>
+ <channel id="alarmActive" typeId="alarmIsActiveChannel"></channel>
+ <channel id="alarmSource" typeId="alarmSourceChannel"></channel>
+ </channels>
+ </channel-group-type>
+
+ <!-- Contact Sensor -->
+ <channel-group-type id="contactSensor">
+ <label>Contact Sensor</label>
+ <description>Door/Window Contact Sensor Channels</description>
+ <channels>
+ <channel id="isOpen" typeId="isOpenChannel"></channel>
+ <channel id="contactOpened" typeId="contactOpenEvent"></channel>
+ <channel id="contactClosed" typeId="contactCloseEvent"></channel>
+ </channels>
+ </channel-group-type>
+
+ <!-- Temperature Sensor -->
+ <channel-group-type id="temperatureSensor">
+ <label>Wheater Sensor</label>
+ <description>Temperature and Humidity Sensor Channels</description>
+ <channels>
+ <channel id="currentTemp" typeId="temperatureChannel"></channel>
+ <channel id="currentHumidity" typeId="humidityChannel"></channel>
+ </channels>
+ </channel-group-type>
+
+</thing:thing-descriptions>
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-Status 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>
-
- <!--Device-Status Channel Type (Small) -->
- <channel-group-type id="deviceStateS">
- <label>Device State</label>
- <description>Information about the device</description>
- <channels>
- <channel id="wifiSignal" typeId="system.signal-strength"/>
- <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>
-
- <channel-group-type id="powerStrip">
- <label>SmartPlug</label>
- <description>Tapo Smart Plug Power Outlet</description>
- <channels>
- <channel id="output1" typeId="outputChannel">
- <label>Output Switch 1</label>
- <description>Switches the power state on/off of the first socket</description>
- </channel>
- <channel id="output2" typeId="outputChannel">
- <label>Output Switch 2</label>
- <description>Switches the power state on/off of the second socket</description>
- </channel>
- <channel id="output3" typeId="outputChannel">
- <label>Output Switch 3</label>
- <description>Switches the power state on/off of the third socket</description>
- </channel>
- </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>
-
- <!-- Energy Monitor -->
- <channel-group-type id="energyMonitor">
- <label>Energy Usage</label>
- <description>Energy and Power usage</description>
- <channels>
- <channel id="actualPower" typeId="actualPowerChannel"></channel>
- <channel id="todayEnergyUsage" typeId="todayEnergyUsageChannel"></channel>
- <channel id="todayRuntime" typeId="todayRuntimeChannel"></channel>
- </channels>
- </channel-group-type>
-
<!-- ############################### CHANNELS ############################### -->
<!-- ACTOR CHANNEL TYPES -->
<state readOnly="false"/>
</channel-type>
+ <!-- Color Bulb Mode -->
+ <channel-type id="colorBulbMode">
+ <item-type>String</item-type>
+ <label>Mode</label>
+ <description>Working mode of device</description>
+ <category>LightBulb</category>
+ <state readOnly="false"/>
+ <command>
+ <options>
+ <option value="WHITE_LIGHT">White</option>
+ <option value="COLOR_LIGHT">Color</option>
+ <option value="LIGHT_FX">Effects</option>
+ </options>
+ </command>
+ </channel-type>
+
<!-- Color Channel Type -->
<channel-type id="colorChannel">
<item-type>Color</item-type>
<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>
+ <description>This channel supports adjusting the color temperature from 2200K to 6500K.</description>
<category>LightBulb</category>
- <state min="2500" max="6500" pattern="%d K"/>
+ <state min="2200" max="6500" pattern="%d K"/>
+ </channel-type>
+
+ <!-- SENSOR CHANNEL TYPES -->
+ <!-- SmartContact "isOpen" Channel Type -->
+ <channel-type id="isOpenChannel">
+ <item-type>Switch</item-type>
+ <label>Is Open</label>
+ <description>Contact (Window/Door) is Open</description>
+ <category>Switch</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <!-- Temperature Channel Type -->
+ <channel-type id="temperatureChannel">
+ <item-type>Number:Temperature</item-type>
+ <label>Temperature</label>
+ <description>Temperature as measured by the sensor</description>
+ <category>Temperature</category>
+ <state readOnly="true" pattern="%.1f %unit%" step="0.1"/>
+ </channel-type>
+
+ <!-- Humidity Channel Type -->
+ <channel-type id="humidityChannel">
+ <item-type>Number:Dimensionless</item-type>
+ <label>Humidity</label>
+ <description>Current relative humidity</description>
+ <category>Humidity</category>
+ <state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<state readOnly="true"/>
</channel-type>
+ <!-- is Online -->
+ <channel-type id="isOnlineChannel">
+ <item-type>Switch</item-type>
+ <label>Is Online</label>
+ <description>Device is Online</description>
+ <state readOnly="true"></state>
+ </channel-type>
- <!-- DEVICE-STATE CHANNEL TYPES -->
+ <!-- battery Level -->
+ <channel-type id="batteryLowChannel">
+ <item-type>Switch</item-type>
+ <label>Battery Low</label>
+ <description>Battery of device is low</description>
+ <category>LowBattery</category>
+ <state readOnly="true"></state>
+ </channel-type>
+
+ <!-- ENERGY USAGE CHANNEL TYPES -->
<!-- actual power usage -->
<channel-type id="actualPowerChannel">
<item-type>Number:Power</item-type>
<state readOnly="true" pattern="%.0f %unit%"></state>
</channel-type>
+ <!-- month energy usage -->
+ <channel-type id="monthEnergyUsageChannel">
+ <item-type>Number:Energy</item-type>
+ <label>Month Usage</label>
+ <description>Energy usage last month</description>
+ <category>Energy</category>
+ <state readOnly="true" pattern="%.2f %unit%"></state>
+ </channel-type>
+
+ <!-- month runtime -->
+ <channel-type id="monthRuntimeChannel">
+ <item-type>Number:Time</item-type>
+ <label>Month Runtime</label>
+ <description>Runtime last month (On-Time)</description>
+ <category>Time</category>
+ <state readOnly="true" pattern="%.0f %unit%"></state>
+ </channel-type>
+
<!-- ADVANCED SETTING CHANNELS -->
<!-- device led -->
<channel-type id="led" advanced="true">
<category>Switch</category>
</channel-type>
+ <!-- ALARM CHANNEL CHANNELS -->
+ <!-- has active alarm -->
+ <channel-type id="alarmIsActiveChannel">
+ <item-type>Switch</item-type>
+ <label>Active Alarm</label>
+ <description>Device has an active alarm</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <!-- active alarm source -->
+ <channel-type id="alarmSourceChannel">
+ <item-type>String</item-type>
+ <label>Alarm Source</label>
+ <description>Source of active alarm</description>
+ <category>Alarm</category>
+ <state readOnly="true"/>
+ </channel-type>
+
+ <!-- TRIGGER CHANNELS -->
+ <!-- contact opened -->
+ <channel-type id="batteryLowEvent">
+ <kind>Trigger</kind>
+ <label>Battery Low</label>
+ <description>Battery state has changed to low. Replace Battery</description>
+ </channel-type>
+ <!-- contact opened -->
+ <channel-type id="contactOpenEvent">
+ <kind>Trigger</kind>
+ <label>Contact Opened</label>
+ <description>Event is fired if contact changes from closed to open</description>
+ </channel-type>
+ <!-- contact closed -->
+ <channel-type id="contactCloseEvent">
+ <kind>Trigger</kind>
+ <label>Contact Closed</label>
+ <description>Event is fired if contact changes from open to closed</description>
+ </channel-type>
+
</thing:thing-descriptions>
--- /dev/null
+ {
+ "id": "TapoStrip_1MClvV18i15Jq3bvJVf0eP",
+ "name": "Aurora",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 120,
+ 100,
+ 100
+ ],
+ [
+ 240,
+ 100,
+ 100
+ ],
+ [
+ 260,
+ 100,
+ 100
+ ],
+ [
+ 280,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 4,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 0,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 120,
+ 100,
+ 100
+ ],
+ [
+ 240,
+ 100,
+ 100
+ ],
+ [
+ 260,
+ 100,
+ 100
+ ],
+ [
+ 280,
+ 100,
+ 100
+ ]
+ ],
+ "spread": 7,
+ "transition": 1500,
+ "transition_range": null,
+ "type": "sequence",
+ "trans_sequence": null,
+ "run_time": null
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_6DlumDwO2NdfHppy50vJtu",
+ "name": "Bubbling Cauldron",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 100,
+ 100,
+ 100
+ ],
+ [
+ 270,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 50,
+ 100
+ ],
+ "backgrounds": [
+ [
+ 270,
+ 40,
+ 50
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": 1000,
+ "hue_range": [
+ 100,
+ 270
+ ],
+ "init_states": [
+ [
+ 270,
+ 100,
+ 100
+ ]
+ ],
+ "random_seed": 24,
+ "repeat_times": null,
+ "saturation_range": [
+ 80,
+ 100
+ ],
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 200,
+ "transition_range": null,
+ "type": "random",
+ "trans_sequence": null,
+ "run_time": null
+}
\ No newline at end of file
--- /dev/null
+{
+ "id": "TapoStrip_6Dy0Nc45vlhFPEzG021Pe9",
+ "name": "Candy Cane",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 1,
+ "duration": 700,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 0,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15
+ ],
+ "sequence": [
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 100
+ ],
+ [
+ 360,
+ 81,
+ 100
+ ]
+ ],
+ "spread": 1,
+ "transition": 500,
+ "transition_range": null,
+ "type": "sequence"
+ }
\ No newline at end of file
--- /dev/null
+{
+ "id": "TapoStrip_5zkiG6avJ1IbhjiZbRlWvh",
+ "name": "Christmas",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 136,
+ 98,
+ 100
+ ],
+ [
+ 350,
+ 97,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 50,
+ 100
+ ],
+ "backgrounds": [
+ [
+ 136,
+ 98,
+ 75
+ ],
+ [
+ 136,
+ 0,
+ 0
+ ],
+ [
+ 350,
+ 0,
+ 100
+ ],
+ [
+ 350,
+ 97,
+ 94
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 5000,
+ "expansion_strategy": 1,
+ "fadeoff": 2000,
+ "hue_range": [
+ 136,
+ 146
+ ],
+ "init_states": [
+ [
+ 136,
+ 0,
+ 100
+ ]
+ ],
+ "random_seed": 100,
+ "repeat_times": null,
+ "saturation_range": [
+ 90,
+ 100
+ ],
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 0,
+ "transition_range": null,
+ "type": "random"
+ }
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_3Gk6CmXOXbjCiwz9iD543C",
+ "name": "Grandma's Christmas Lights",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 30,
+ 100,
+ 100
+ ],
+ [
+ 240,
+ 100,
+ 100
+ ],
+ [
+ 130,
+ 100,
+ 100
+ ],
+ [
+ 0,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 1,
+ "duration": 5000,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 0,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 30,
+ 100,
+ 100
+ ],
+ [
+ 30,
+ 0,
+ 0
+ ],
+ [
+ 30,
+ 0,
+ 0
+ ],
+ [
+ 240,
+ 100,
+ 100
+ ],
+ [
+ 240,
+ 0,
+ 0
+ ],
+ [
+ 240,
+ 0,
+ 0
+ ],
+ [
+ 240,
+ 0,
+ 100
+ ],
+ [
+ 240,
+ 0,
+ 0
+ ],
+ [
+ 240,
+ 0,
+ 0
+ ],
+ [
+ 130,
+ 100,
+ 100
+ ],
+ [
+ 130,
+ 0,
+ 0
+ ],
+ [
+ 130,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 100,
+ 100
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ],
+ [
+ 0,
+ 0,
+ 0
+ ]
+ ],
+ "spread": 1,
+ "transition": 100,
+ "transition_range": null,
+ "type": "sequence"
+}
\ No newline at end of file
--- /dev/null
+[
+ {
+ "id":"L1",
+ "name":"Party",
+ "enable":true
+ },
+ {
+ "id":"L2",
+ "name":"Relax",
+ "enable":true
+ }
+ ]
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_4HVKmMc6vEzjm36jXaGwMs",
+ "name": "Flicker",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 30,
+ 81,
+ 100
+ ],
+ [
+ 40,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 50,
+ 100
+ ],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": null,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": [
+ 30,
+ 40
+ ],
+ "init_states": [
+ [
+ 30,
+ 81,
+ 80
+ ]
+ ],
+ "random_seed": null,
+ "repeat_times": null,
+ "saturation_range": [
+ 100,
+ 100
+ ],
+ "segment_length": null,
+ "segments": [
+ 1
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 0,
+ "transition_range": [
+ 375,
+ 500
+ ],
+ "type": "random"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_2YTk4wramLKv5XZ9KFDVYm",
+ "name": "Hanukkah",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 200,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 50,
+ 100
+ ],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": null,
+ "duration": 1500,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": [
+ 200,
+ 210
+ ],
+ "init_states": [
+ [
+ 35,
+ 81,
+ 80
+ ]
+ ],
+ "random_seed": null,
+ "repeat_times": null,
+ "saturation_range": [
+ 0,
+ 100
+ ],
+ "segment_length": null,
+ "segments": [
+ 1
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 0,
+ "transition_range": [
+ 400,
+ 500
+ ],
+ "type": "random"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_4rJ6JwC7I9st3tQ8j4lwlI",
+ "name": "Haunted Mansion",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 45,
+ 10,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 0,
+ 80
+ ],
+ "backgrounds": [
+ [
+ 45,
+ 10,
+ 100
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 0,
+ "expansion_strategy": 2,
+ "fadeoff": 200,
+ "hue_range": [
+ 45,
+ 45
+ ],
+ "init_states": [
+ [
+ 45,
+ 10,
+ 100
+ ]
+ ],
+ "random_seed": 1,
+ "repeat_times": null,
+ "saturation_range": [
+ 10,
+ 10
+ ],
+ "segment_length": null,
+ "segments": [
+ 80
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 0,
+ "transition_range": [
+ 50,
+ 1500
+ ],
+ "type": "random"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_7UcYLeJbiaxVIXCxr21tpx",
+ "name": "Icicle",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 190,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 4,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 0,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 190,
+ 100,
+ 70
+ ],
+ [
+ 190,
+ 100,
+ 70
+ ],
+ [
+ 190,
+ 30,
+ 50
+ ],
+ [
+ 190,
+ 100,
+ 70
+ ],
+ [
+ 190,
+ 100,
+ 70
+ ]
+ ],
+ "spread": 3,
+ "transition": 400,
+ "transition_range": null,
+ "type": "sequence"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_7OGzfSfnOdhoO2ri4gOHWn",
+ "name": "Lightning",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 210,
+ 10,
+ 100
+ ],
+ [
+ 200,
+ 50,
+ 100
+ ],
+ [
+ 200,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 90,
+ 100
+ ],
+ "backgrounds": [
+ [
+ 200,
+ 100,
+ 100
+ ],
+ [
+ 200,
+ 50,
+ 10
+ ],
+ [
+ 210,
+ 10,
+ 50
+ ],
+ [
+ 240,
+ 10,
+ 0
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": 150,
+ "hue_range": [
+ 240,
+ 240
+ ],
+ "init_states": [
+ [
+ 240,
+ 30,
+ 100
+ ]
+ ],
+ "random_seed": 600,
+ "repeat_times": null,
+ "saturation_range": [
+ 10,
+ 11
+ ],
+ "segment_length": null,
+ "segments": [
+ 7,
+ 20,
+ 23,
+ 32,
+ 34,
+ 35,
+ 49,
+ 65,
+ 66,
+ 74,
+ 80
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 50,
+ "transition_range": null,
+ "type": "random"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_0fOleCdwSgR0nfjkReeYfw",
+ "name": "Ocean",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 198,
+ 84,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 3,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 0,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 198,
+ 84,
+ 30
+ ],
+ [
+ 198,
+ 70,
+ 30
+ ],
+ [
+ 198,
+ 10,
+ 30
+ ]
+ ],
+ "spread": 16,
+ "transition": 2000,
+ "transition_range": null,
+ "type": "sequence"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_7CC5y4lsL8pETYvmz7UOpQ",
+ "name": "Rainbow",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 0,
+ 100,
+ 100
+ ],
+ [
+ 100,
+ 100,
+ 100
+ ],
+ [
+ 200,
+ 100,
+ 100
+ ],
+ [
+ 300,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 1,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 0,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 0,
+ 100,
+ 100
+ ],
+ [
+ 100,
+ 100,
+ 100
+ ],
+ [
+ 200,
+ 100,
+ 100
+ ],
+ [
+ 300,
+ 100,
+ 100
+ ]
+ ],
+ "spread": 12,
+ "transition": 1500,
+ "transition_range": null,
+ "type": "sequence"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_1t2nWlTBkV8KXBZ0TWvBjs",
+ "name": "Raindrop",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 200,
+ 10,
+ 100
+ ],
+ [
+ 200,
+ 20,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 10,
+ 30
+ ],
+ "backgrounds": [
+ [
+ 200,
+ 40,
+ 0
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 0,
+ "expansion_strategy": 1,
+ "fadeoff": 1000,
+ "hue_range": [
+ 200,
+ 200
+ ],
+ "init_states": [
+ [
+ 200,
+ 40,
+ 100
+ ]
+ ],
+ "random_seed": 24,
+ "repeat_times": null,
+ "saturation_range": [
+ 10,
+ 20
+ ],
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 1000,
+ "transition_range": null,
+ "type": "random"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_1nL6GqZ5soOxj71YDJOlZL",
+ "name": "Spring",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 0,
+ 30,
+ 100
+ ],
+ [
+ 130,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 90,
+ 100
+ ],
+ "backgrounds": [
+ [
+ 130,
+ 100,
+ 40
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 600,
+ "expansion_strategy": 1,
+ "fadeoff": 1000,
+ "hue_range": [
+ 0,
+ 90
+ ],
+ "init_states": [
+ [
+ 80,
+ 30,
+ 100
+ ]
+ ],
+ "random_seed": 20,
+ "repeat_times": null,
+ "saturation_range": [
+ 30,
+ 100
+ ],
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 0,
+ "transition_range": [
+ 2000,
+ 6000
+ ],
+ "type": "random"
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_1OVSyXIsDxrt4j7OxyRvqi",
+ "name": "Sunrise",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 30,
+ 0,
+ 100
+ ],
+ [
+ 30,
+ 95,
+ 100
+ ],
+ [
+ 0,
+ 100,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 1,
+ "duration": 600,
+ "expansion_strategy": 2,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 1,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 0,
+ 100,
+ 5
+ ],
+ [
+ 0,
+ 100,
+ 5
+ ],
+ [
+ 10,
+ 100,
+ 6
+ ],
+ [
+ 15,
+ 100,
+ 7
+ ],
+ [
+ 20,
+ 100,
+ 8
+ ],
+ [
+ 20,
+ 100,
+ 10
+ ],
+ [
+ 30,
+ 100,
+ 12
+ ],
+ [
+ 30,
+ 95,
+ 15
+ ],
+ [
+ 30,
+ 90,
+ 20
+ ],
+ [
+ 30,
+ 80,
+ 25
+ ],
+ [
+ 30,
+ 75,
+ 30
+ ],
+ [
+ 30,
+ 70,
+ 40
+ ],
+ [
+ 30,
+ 60,
+ 50
+ ],
+ [
+ 30,
+ 50,
+ 60
+ ],
+ [
+ 30,
+ 20,
+ 70
+ ],
+ [
+ 30,
+ 0,
+ 100
+ ]
+ ],
+ "spread": 1,
+ "transition": 60000,
+ "transition_range": null,
+ "type": "pulse",
+ "trans_sequence": [],
+ "run_time": 0
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_5NiN0Y8GAUD78p4neKk9EL",
+ "name": "Sunset",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 0,
+ 100,
+ 100
+ ],
+ [
+ 30,
+ 95,
+ 100
+ ],
+ [
+ 30,
+ 0,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [],
+ "backgrounds": [],
+ "custom": 0,
+ "direction": 1,
+ "duration": 600,
+ "expansion_strategy": 2,
+ "fadeoff": null,
+ "hue_range": null,
+ "init_states": [],
+ "random_seed": null,
+ "repeat_times": 1,
+ "saturation_range": null,
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": [
+ [
+ 30,
+ 0,
+ 100
+ ],
+ [
+ 30,
+ 20,
+ 100
+ ],
+ [
+ 30,
+ 50,
+ 99
+ ],
+ [
+ 30,
+ 60,
+ 98
+ ],
+ [
+ 30,
+ 70,
+ 97
+ ],
+ [
+ 30,
+ 75,
+ 95
+ ],
+ [
+ 30,
+ 80,
+ 93
+ ],
+ [
+ 30,
+ 90,
+ 90
+ ],
+ [
+ 30,
+ 95,
+ 85
+ ],
+ [
+ 30,
+ 100,
+ 80
+ ],
+ [
+ 20,
+ 100,
+ 70
+ ],
+ [
+ 20,
+ 100,
+ 60
+ ],
+ [
+ 15,
+ 100,
+ 50
+ ],
+ [
+ 10,
+ 100,
+ 40
+ ],
+ [
+ 0,
+ 100,
+ 30
+ ],
+ [
+ 0,
+ 100,
+ 0
+ ]
+ ],
+ "spread": 1,
+ "transition": 60000,
+ "transition_range": null,
+ "type": "pulse",
+ "trans_sequence": [],
+ "run_time": 0
+}
\ No newline at end of file
--- /dev/null
+ {
+ "id": "TapoStrip_2q1Vio9sSjHmaC7JS9d30l",
+ "name": "Valentines",
+ "brightness": 100,
+ "display_colors": [
+ [
+ 340,
+ 20,
+ 100
+ ],
+ [
+ 20,
+ 50,
+ 100
+ ],
+ [
+ 0,
+ 100,
+ 100
+ ],
+ [
+ 340,
+ 40,
+ 100
+ ]
+ ],
+ "enable": 1,
+ "bAdjusted": null,
+ "brightness_range": [
+ 90,
+ 100
+ ],
+ "backgrounds": [
+ [
+ 340,
+ 20,
+ 50
+ ],
+ [
+ 20,
+ 50,
+ 50
+ ],
+ [
+ 0,
+ 100,
+ 50
+ ]
+ ],
+ "custom": 0,
+ "direction": null,
+ "duration": 600,
+ "expansion_strategy": 1,
+ "fadeoff": 3000,
+ "hue_range": [
+ 340,
+ 340
+ ],
+ "init_states": [
+ [
+ 340,
+ 30,
+ 100
+ ]
+ ],
+ "random_seed": 100,
+ "repeat_times": null,
+ "saturation_range": [
+ 30,
+ 40
+ ],
+ "segment_length": null,
+ "segments": [
+ 0
+ ],
+ "sequence": null,
+ "spread": null,
+ "transition": 2000,
+ "transition_range": null,
+ "type": "random",
+ "trans_sequence": null,
+ "run_time": null
+}
\ No newline at end of file
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.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);
- }
-
- /**
- * activate
- */
- @Override
- public void activate() {
- TapoBridgeConfiguration config = bridge.getBridgeConfig();
- if (config.cloudDiscovery || config.udpDiscovery) {
- startBackgroundDiscovery();
- }
- }
-
- /**
- * deactivate
- */
- @Override
- public void deactivate() {
- super.deactivate();
- }
-
- @Override
- public void setThingHandler(@Nullable ThingHandler handler) {
- if (handler instanceof TapoBridgeHandler tapoBridge) {
- 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_JSON_KEY_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_JSON_KEY_FW).getAsString());
- properties.put(Thing.PROPERTY_HARDWARE_VERSION, device.get(CLOUD_JSON_KEY_HW).getAsString());
- properties.put(Thing.PROPERTY_MODEL_ID, deviceModel);
- properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.get(CLOUD_JSON_KEY_ID).getAsString());
-
- logger.debug("device {} discovered", deviceModel);
- if (tapoBridge != null) {
- ThingUID bridgeUID = tapoBridge.getUID();
- ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceMAC);
- return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
- .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label)
- .build();
- } else {
- ThingUID thingUID = new ThingUID(BINDING_ID, deviceMAC);
- return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
- .withRepresentationProperty(DEVICE_REPRESENTATION_PROPERTY).withLabel(label).build();
- }
- }
-
- /**
- * 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_JSON_KEY_MODEL).getAsString();
- deviceModel = deviceModel.replaceAll("\\(.*\\)", ""); // replace (DE)
- deviceModel = deviceModel.replace("Tapo", "");
- deviceModel = deviceModel.replace("Series", "");
- deviceModel = deviceModel.trim();
- deviceModel = deviceModel.replace(" ", "_");
- return deviceModel;
- } catch (Exception e) {
- logger.debug("error getDeviceModel", e);
- return "";
- }
- }
-
- /**
- * GET DEVICE LABEL
- *
- * @param device JsonObject with deviceData
- * @return String with DeviceLabel
- */
- protected String getDeviceLabel(JsonObject device) {
- try {
- String deviceLabel = "";
- String deviceModel = getDeviceModel(device);
- ThingTypeUID deviceUID = new ThingTypeUID(BINDING_ID, deviceModel);
-
- if (SUPPORTED_SMART_PLUG_UIDS.contains(deviceUID)) {
- deviceLabel = DEVICE_DESCRIPTION_SMART_PLUG;
- } else if (SUPPORTED_WHITE_BULB_UIDS.contains(deviceUID)) {
- deviceLabel = DEVICE_DESCRIPTION_WHITE_BULB;
- } else if (SUPPORTED_COLOR_BULB_UIDS.contains(deviceUID)) {
- deviceLabel = DEVICE_DESCRIPTION_COLOR_BULB;
- }
- return DEVICE_VENDOR + " " + deviceModel + " " + deviceLabel;
- } catch (Exception e) {
- logger.debug("error getDeviceLabel", e);
- return "";
- }
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import java.util.Collection;
-import java.util.Set;
-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 HttpClient httpClient;
- private TapoBridgeConfiguration config;
- 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();
- 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 = getConfigAs(TapoBridgeConfiguration.class);
- 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 Set.of(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.reconnectInterval;
- 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.discoveryInterval;
- if (config.cloudDiscovery && 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.cloudDiscovery) {
- logger.trace("{} discover devicelist from cloud", this.uid);
- deviceList = getDeviceListCloud();
- } else if (config.udpDiscovery) {
- logger.trace("{} discover devicelist from udp", this.uid);
- deviceList = getDeviceListUDP();
- }
- return deviceList;
- }
-
- /**
- * GET DEVICELIST FROM CLOUD
- * returns all devices stored in cloud
- *
- * @return deviceList from cloud
- */
- private JsonArray getDeviceListCloud() {
- logger.trace("{} getDeviceList from cloud", this.uid);
- bridgeError.reset(); // reset ErrorHandler
- JsonArray deviceList = new JsonArray();
- if (loginCloud()) {
- deviceList = this.cloudConnector.getDeviceList();
- }
- return deviceList;
- }
-
- /**
- * GET DEVICELIST UDP
- * return devices discovered by UDP
- *
- * @return deviceList from udp
- */
- public JsonArray getDeviceListUDP() {
- bridgeError.reset(); // reset ErrorHandler
- TapoUDP udpDiscovery = new TapoUDP(credentials);
- return udpDiscovery.udpScan();
- }
-
- /***********************************
- *
- * BRIDGE GETTERS
- *
- ************************************/
-
- public TapoCredentials getCredentials() {
- return this.credentials;
- }
-
- public HttpClient getHttpClient() {
- return this.httpClient;
- }
-
- public ThingUID getUID() {
- return getThing().getUID();
- }
-
- public TapoBridgeConfiguration getBridgeConfig() {
- return this.config;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.device;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.util.HashMap;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
-import org.openhab.core.library.types.DecimalType;
-import org.openhab.core.library.types.HSBType;
-import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.PercentType;
-import org.openhab.core.library.unit.Units;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * TAPO Universal-Device
- * universal device for testing pruposes
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoUniversalDevice extends TapoDevice {
- private final Logger logger = LoggerFactory.getLogger(TapoUniversalDevice.class);
-
- // CHANNEL LIST
- public static final String CHANNEL_GROUP_DEBUG = "debug";
- public static final String CHANNEL_RESPONSE = "deviceResponse";
- public static final String CHANNEL_COMMAND = "deviceCommand";
-
- /**
- * Constructor
- *
- * @param thing Thing object representing device
- */
- public TapoUniversalDevice(Thing thing) {
- super(thing);
- }
-
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- logger.debug("({}) handleCommand '{}' for channelUID {}", uid, command.toString(), channelUID.getId());
- Boolean refreshInfo = false;
-
- String channel = channelUID.getIdWithoutGroup();
- if (command instanceof RefreshType) {
- refreshInfo = true;
- } else {
- switch (channel) {
- case CHANNEL_OUTPUT:
- connector.sendDeviceCommand(JSON_KEY_ON, command == OnOffType.ON);
- refreshInfo = true;
- break;
- case CHANNEL_BRIGHTNESS:
- if (command instanceof PercentType percentCommand) {
- Float percent = percentCommand.floatValue();
- setBrightness(percent.intValue()); // 0..100% = 0..100
- refreshInfo = true;
- } else if (command instanceof DecimalType decimalCommand) {
- setBrightness(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR_TEMP:
- if (command instanceof DecimalType decimalCommand) {
- setColorTemp(decimalCommand.intValue());
- refreshInfo = true;
- }
- break;
- case CHANNEL_COLOR:
- if (command instanceof HSBType hsbCommand) {
- setColor(hsbCommand);
- refreshInfo = true;
- }
- break;
- case CHANNEL_COMMAND:
- String[] cmd = command.toString().split(":");
- if (cmd.length == 1) {
- connector.sendCustomQuery(cmd[0]);
- } else if (cmd.length == 2) {
- connector.sendDeviceCommand(cmd[0], cmd[1]);
- } else {
- logger.warn("({}) wrong command format '{}'", uid, command.toString());
- }
- break;
- default:
- logger.warn("({}) command type '{}' not supported for channel '{}'", uid, command.toString(),
- channelUID.getId());
- }
- }
-
- /* refreshInfo */
- if (refreshInfo) {
- queryDeviceInfo();
- }
- }
-
- /**
- * SET BRIGHTNESS
- *
- * @param newBrightness percentage 0-100 of new brightness
- */
- protected void setBrightness(Integer newBrightness) {
- /* switch off if 0 */
- if (newBrightness == 0) {
- connector.sendDeviceCommand(JSON_KEY_ON, false);
- } else {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_BRIGHTNESS, newBrightness);
- connector.sendDeviceCommands(newState);
- }
- }
-
- /**
- * SET COLOR
- *
- * @param command
- */
- protected void setColor(HSBType command) {
- HashMap<String, Object> newState = new HashMap<>();
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_HUE, command.getHue());
- newState.put(JSON_KEY_SATURATION, command.getSaturation());
- newState.put(JSON_KEY_BRIGHTNESS, command.getBrightness());
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * SET COLORTEMP
- *
- * @param colorTemp (Integer) in Kelvin
- */
- protected void setColorTemp(Integer colorTemp) {
- HashMap<String, Object> newState = new HashMap<>();
- colorTemp = limitVal(colorTemp, BULB_MIN_COLORTEMP, BULB_MAX_COLORTEMP);
- newState.put(JSON_KEY_ON, true);
- newState.put(JSON_KEY_COLORTEMP, colorTemp);
- connector.sendDeviceCommands(newState);
- }
-
- /**
- * SET DEVICE INFOs to device
- *
- * @param deviceInfo
- */
- @Override
- public void setDeviceInfo(TapoDeviceInfo deviceInfo) {
- devicePropertiesChanged(deviceInfo);
- handleConnectionState();
- }
-
- /**
- * Handle full responsebody received from connector
- *
- * @param responseBody
- */
- @Override
- public void responsePasstrough(String responseBody) {
- logger.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),
- getTimeType(deviceInfo.getOnTime(), Units.SECOND));
- publishState(getChannelID(CHANNEL_GROUP_DEVICE, CHANNEL_OVERHEAT),
- getDecimalType(deviceInfo.isOverheated() ? 1 : 0));
- }
-
- /***********************************
- *
- * CHANNELS
- *
- ************************************/
- /**
- * Get ChannelID including group
- *
- * @param group String channel-group
- * @param channel String channel-name
- * @return String channelID
- */
- @Override
- protected String getChannelID(String group, String channel) {
- return group + "#" + channel;
- }
-
- /**
- * Get Channel from ChannelID
- *
- * @param channelID String channelID
- * @return String channel-name
- */
- @Override
- protected String getChannelFromID(ChannelUID channelID) {
- String channel = channelID.getIdWithoutGroup();
- channel = channel.replace(CHANNEL_GROUP_ACTUATOR + "#", "");
- channel = channel.replace(CHANNEL_GROUP_DEVICE + "#", "");
- channel = channel.replace(CHANNEL_GROUP_DEBUG + "#", "");
- return channel;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.api;
-
-import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
-
-import java.util.Dictionary;
-import java.util.Set;
-
-import javax.jmdns.ServiceInfo;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.config.discovery.DiscoveryResult;
-import org.openhab.core.config.discovery.DiscoveryService;
-import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
-import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
-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.Modified;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Handler class for TAPO Smart Home thing discovery over mDNS
- *
- * @author Christian Wild - Initial contribution
- */
-@Component(configurationPid = "discovery.tapocontrol")
-@NonNullByDefault
-public class TapoMDNS implements MDNSDiscoveryParticipant {
- private final Logger logger = LoggerFactory.getLogger(TapoMDNS.class);
- private boolean isAutoDiscoveryEnabled = true;
-
- @Activate
- protected void activate(ComponentContext componentContext) {
- activateOrModifyService(componentContext);
- }
-
- @Modified
- protected void modified(ComponentContext componentContext) {
- activateOrModifyService(componentContext);
- }
-
- private void activateOrModifyService(ComponentContext componentContext) {
- Dictionary<String, @Nullable Object> properties = componentContext.getProperties();
- String autoDiscoveryPropertyValue = (String) properties
- .get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY);
- if (autoDiscoveryPropertyValue != null && !autoDiscoveryPropertyValue.isBlank()) {
- isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue);
- }
- }
-
- @Override
- public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
- return SUPPORTED_THING_TYPES_UIDS;
- }
-
- @Override
- public String getServiceType() {
- return "NULL";
- }
-
- @Override
- public @Nullable ThingUID getThingUID(ServiceInfo service) {
- ThingTypeUID thingTypeUID = getThingType(service);
- if (thingTypeUID != null) {
- String id = service.getPropertyString(PROPERTY_FAMILY); // device id
- return new ThingUID(thingTypeUID, id);
- }
- return null;
- }
-
- private @Nullable ThingTypeUID getThingType(final ServiceInfo service) {
- String model = service.getPropertyString(PROPERTY_FAMILY); // model
- logger.debug("found Type: {}", model);
- if (model == null) {
- return null;
- }
- return L510_THING_TYPE;
- }
-
- @Override
- public @Nullable DiscoveryResult createResult(ServiceInfo service) {
- if (isAutoDiscoveryEnabled) {
- ThingUID uid = getThingUID(service);
- if (uid != null) {
- String host = service.getHostAddresses()[0];
- int port = service.getPort();
- logger.debug("device Found: {} {}", host, port);
- }
- }
- return null;
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.api;
-
-import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
-
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.InetAddress;
-import java.net.SocketTimeoutException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.SecureRandom;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.interfaces.RSAPublicKey;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-
-/**
- * Handler class for TAPO Smart Home device UDP-connections.
- * THIS IS FOR TESTING
- *
- * @author Christian Wild - Initial contribution
- */
-@NonNullByDefault
-public class TapoUDP {
- private final Logger logger = LoggerFactory.getLogger(TapoUDP.class);
- private static final Integer BROADCAST_TIMEOUT_MS = 5000;
- private static final Integer BROADCAST_DISCOVERY_PORT = 20002; // int
- private static final String BROADCAST_IP = "255.255.255.255";
- private static final String DISCOVERY_MESSAGE_KEY = "rsa_key";
- private static final String DISCOVERY_MESSAGE_START_BYTES = "0200000101e5110001cb8c577dd7deb8";
- private static final Integer BUFFER_SIZE = 501;
- private TapoCredentials credentials;
-
- public TapoUDP(TapoCredentials credentials) {
- this.credentials = credentials; // new TapoCredentials();
- }
-
- public JsonArray udpScan() {
- try {
- DatagramSocket udpSocket = new DatagramSocket();
- udpSocket.setSoTimeout(BROADCAST_TIMEOUT_MS);
- udpSocket.setBroadcast(true);
-
- /* create payload for handshake */
- String publicKey = credentials.getPublicKey();
- publicKey = generateOwnRSAKey(); // credentials.getPublicKey();
- JsonObject parameters = new JsonObject();
- JsonObject messageObject = new JsonObject();
- parameters.addProperty(DISCOVERY_MESSAGE_KEY, publicKey);
- messageObject.add("params", parameters);
-
- String discoveryMessage = messageObject.toString();
-
- byte[] startByte = hexStringToByteArray(DISCOVERY_MESSAGE_START_BYTES);
- byte[] message = discoveryMessage.getBytes("UTF-8");
- byte[] sendData = new byte[startByte.length + message.length];
- System.arraycopy(startByte, 0, sendData, 0, startByte.length);
- System.arraycopy(message, 0, sendData, startByte.length, message.length);
-
- DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
- InetAddress.getByName(BROADCAST_IP), BROADCAST_DISCOVERY_PORT);
-
- udpSocket.send(sendPacket);
-
- while (true) {
- // Wait for a response
- byte[] recvBuf = new byte[BUFFER_SIZE];
- DatagramPacket receivePacket;
- try {
- receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
- udpSocket.receive(receivePacket);
- } catch (SocketTimeoutException e) {
- udpSocket.close();
- return new JsonArray();
- } catch (Exception e) {
- udpSocket.close();
- return new JsonArray();
- }
-
- // Check if the message is correct
- String responseMessage = new String(receivePacket.getData(), "UTF-8").trim();
-
- if (responseMessage.length() == 0) {
- udpSocket.close();
- }
- String addressBC = receivePacket.getAddress().getHostAddress();
- gotDeviceAdress(addressBC);
- }
- } catch (Exception e) {
- // handle exception
- }
- return new JsonArray();
- }
-
- private void gotDeviceAdress(String ipAddress) {
- // handle exception
- }
-
- private String generateOwnRSAKey() {
- try {
- logger.trace("generating new keypair");
- KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
- instance.initialize(1536, new SecureRandom());
- KeyPair generateKeyPair = instance.generateKeyPair();
-
- String publicKey = new String(java.util.Base64.getMimeEncoder()
- .encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded()));
- String privateKey = new String(java.util.Base64.getMimeEncoder()
- .encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded()));
- logger.trace("new privateKey: '{}'", privateKey);
- logger.trace("new ublicKey: '{}'", publicKey);
-
- return String.format("-----BEGIN PUBLIC KEY-----%n%s%n-----END PUBLIC KEY-----%n", publicKey);
-
- } catch (Exception e) {
- // couldn't generate own rsa key
- return "";
- }
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.structures;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link TapoBridgeConfiguration} class contains fields mapping bridge configuration parameters.
- *
- * @author Christian Wild - Initial contribution
- */
-
-@NonNullByDefault
-public final class TapoBridgeConfiguration {
- /* THING CONFIGUTATION PROPERTYS */
- public static final String CONFIG_EMAIL = "username";
- public static final String CONFIG_PASS = "password";
- public static final String CONFIG_DISCOVERY_CLOUD = "cloudDiscovery";
- public static final String CONFIG_DISCOVERY_UDP = "udpDiscovery";
- public static final String CONFIG_DISCOVERY_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 cloudDiscovery = false;
- public boolean udpDiscovery = false;
- public int reconnectInterval = CONFIG_CLOUD_FIXED_INTERVAL;
- public int discoveryInterval = 60;
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.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, JSON_KEY_BRIGHTNESS);
- this.colorTemp = jsonObjectToInt(jsonObject, JSON_KEY_COLORTEMP, BULB_MIN_COLORTEMP);
- this.deviceId = jsonObjectToString(jsonObject, JSON_KEY_ID);
- this.deviceOn = jsonObjectToBool(jsonObject, JSON_KEY_ON);
- this.fwVer = jsonObjectToString(jsonObject, JSON_KEY_FW);
- this.hue = jsonObjectToInt(jsonObject, JSON_KEY_HUE);
- this.hwVer = jsonObjectToString(jsonObject, JSON_KEY_HW_VER);
- this.ip = jsonObjectToString(jsonObject, JSON_KEY_IP);
- this.lightEffect = lightEffect.setData(jsonObject);
- this.mac = jsonObjectToString(jsonObject, JSON_KEY_MAC);
- this.model = jsonObjectToString(jsonObject, JSON_KEY_MODEL);
- this.nickname = jsonObjectToString(jsonObject, JSON_KEY_NICKNAME);
- this.onTime = jsonObjectToNumber(jsonObject, JSON_KEY_ONTIME);
- this.overheated = jsonObjectToBool(jsonObject, JSON_KEY_OVERHEAT);
- this.region = jsonObjectToString(jsonObject, JSON_KEY_REGION);
- this.saturation = jsonObjectToInt(jsonObject, JSON_KEY_SATURATION);
- this.signalLevel = jsonObjectToInt(jsonObject, JSON_KEY_SIGNAL_LEVEL);
- this.rssi = jsonObjectToInt(jsonObject, JSON_KEY_RSSI);
- this.timeUsagePast7 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_7);
- this.timeUsagePast30 = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_30);
- this.timeUsageToday = jsonObjectToInt(jsonObject, JSON_KEY_USAGE_TODAY);
- this.type = jsonObjectToString(jsonObject, JSON_KEY_TYPE);
- }
-
- /***********************************
- *
- * GET VALUES
- *
- ************************************/
-
- public Integer getBrightness() {
- return brightness;
- }
-
- public Integer getColorTemp() {
- return colorTemp;
- }
-
- public String getFirmwareVersion() {
- return fwVer;
- }
-
- public String getHardwareVersion() {
- return hwVer;
- }
-
- public HSBType getHSB() {
- DecimalType h = new DecimalType(hue);
- PercentType s = new PercentType(saturation);
- PercentType b = new PercentType(brightness);
- return new HSBType(h, s, b);
- }
-
- public Integer getHue() {
- return hue;
- }
-
- public TapoLightEffect getLightEffect() {
- return lightEffect;
- }
-
- public String getIP() {
- return ip;
- }
-
- public Boolean isOff() {
- return !deviceOn;
- }
-
- public Boolean isOn() {
- return deviceOn;
- }
-
- public Boolean isOverheated() {
- return overheated;
- }
-
- public String getMAC() {
- return formatMac(mac, MAC_DIVISION_CHAR);
- }
-
- public String getModel() {
- return model.replace(" ", "_");
- }
-
- public String getNickname() {
- return nickname;
- }
-
- public Number getOnTime() {
- return onTime;
- }
-
- public String getRegion() {
- return region;
- }
-
- public String getRepresentationProperty() {
- return getMAC();
- }
-
- public Integer getSaturation() {
- return saturation;
- }
-
- public String getSerial() {
- return deviceId;
- }
-
- public Integer getSignalLevel() {
- return signalLevel;
- }
-
- public Integer getRSSI() {
- return rssi;
- }
-
- public Number getTimeUsagePast7() {
- return timeUsagePast7;
- }
-
- public Number getTimeUsagePast30() {
- return timeUsagePast30;
- }
-
- public Number getTimeUsagePastToday() {
- return timeUsageToday;
- }
-
- public String getType() {
- return type;
- }
-
- @Override
- public String toString() {
- return jsonObject.toString();
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2024 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.tapocontrol.internal.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(JSON_KEY_LIGHTNING_EFFECT)) {
- this.jsonObject = jso;
- } else {
- jsonObject = new JsonObject();
- }
- setData();
- return this;
- }
-
- private void setData() {
- this.enable = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ENABLE);
- this.id = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_ID);
- this.name = jsonObjectToString(jsonObject, JSON_KEY_LIGHTNING_EFFECT_NAME);
- this.custom = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_CUSTOM); // jsonObjectToBool
- this.brightness = jsonObjectToInt(jsonObject, JSON_KEY_LIGHTNING_EFFECT_BRIGHNTESS);
- // this.color_temp_range = { 9000, 9000 }; PROPERTY_LIGHNTING_ //:[9000,9000]
- // this.displayColors[] PROPERTY_LIGHNTING_;
- }
-
- /***********************************
- *
- * SET VALUES
- *
- ************************************/
-
- public void setEnable(Boolean enable) {
- this.enable = enable ? 1 : 0;
- }
-
- public void setName(String value) {
- this.name = value;
- }
-
- public void setCustom(Boolean enable) {
- this.custom = enable ? 1 : 0;
- }
-
- public void setBrightness(Integer value) {
- this.brightness = value;
- }
-
- public void setColorTempRange() {
- }
-
- public void setDisplayColors() {
- }
-
- /***********************************
- *
- * GET VALUES
- *
- ************************************/
-
- public Integer getEnable() {
- return this.enable;
- }
-
- public String getId() {
- return this.id;
- }
-
- public String getName() {
- return this.name;
- }
-
- public Integer getCustom() {
- return this.custom;
- }
-
- public Integer getBrightness() {
- return this.brightness;
- }
-
- public Integer[] getColorTempRange() {
- return this.colorTempRange;
- }
-
- public Color[] getDisplayColors() {
- return this.displayColors;
- }
-
- @Override
- public String toString() {
- return jsonObject.toString();
- }
-}
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<config-description:config-descriptions
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
- xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
-
- <config-description uri="thing-type:tapo:device">
- <parameter name="ipAddress" type="text" required="true">
- <context>network-address</context>
- <label>IP Address</label>
- </parameter>
- <parameter name="pollingInterval" type="integer" min="0" max="9999" required="false">
- <label>Refresh Interval</label>
- <description>Refresh interval for refreshing the data in seconds. (0=disabled)</description>
- <default>30</default>
- <advanced>true</advanced>
- </parameter>
- </config-description>
-
- <config-description uri="bridge-type:tapo:bridge">
- <parameter name="username" type="text" required="true">
- <context>email</context>
- <label>Username</label>
- <description>Tapo-Cloud Login User (e-Mail)</description>
- </parameter>
- <parameter name="password" type="text" required="true">
- <context>password</context>
- <label>Password</label>
- <description>Tapo-Cloud Login Password</description>
- </parameter>
- <parameter name="cloudDiscovery" type="boolean" required="false">
- <label>Cloud Discovery</label>
- <description>Use Cloud Discovery-Service</description>
- <default>false</default>
- <advanced>false</advanced>
- </parameter>
- <parameter name="cloudReconnect" type="integer" min="0" max="10080" required="false">
- <label>Cloud Reconnect Interval</label>
- <description>Interval for reconnecting to the Tapo-Cloud in minutes (default 1440 = 24h / 0 = disabled)</description>
- <default>1440</default>
- <advanced>true</advanced>
- </parameter>
- <!--
- <parameter name="discoveryInterval" type="integer" min="0" max="10080" required="false">
- <label>Background Discovery Interval</label>
- <description>Interval background discovery in minutes (default 60 / 0 = disabled)</description>
- <default>60</default>
- <advanced>true</advanced>
- </parameter>
- <parameter name="udpDiscovery" type="boolean" required="false">
- <label>UDP Discovery</label>
- <description>Use UDP Discovery-Service</description>
- <default>false</default>
- <advanced>true</advanced>
- </parameter>
- -->
- </config-description>
-</config-description:config-descriptions>
<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-group id="response" typeId="responseChannels"/>
+ <channel-group id="devicecommand" typeId="commandChannels"/>
</channel-groups>
<representation-property>macAddress</representation-property>
<!-- ############################### CHANNEL-GROUPS ############################### -->
<!-- CHANNEL GROUP TYPES -->
- <!--Device-Statuss Channel Type -->
- <channel-group-type id="commandDebug">
+ <!-- Command Debug Channel Type -->
+ <channel-group-type id="responseChannels" advanced="true">
<label>Device Communication Debug</label>
<description>Device resoponses and command debugging</description>
<channels>
- <channel id="deviceResponse" typeId="deviceResponse"/>
<channel id="deviceCommand" typeId="deviceCommand"/>
+ <channel id="deviceResponse" typeId="deviceResponse"/>
+ </channels>
+ </channel-group-type>
+
+ <!-- Command Debug Channel Type -->
+ <channel-group-type id="commandChannels" advanced="true">
+ <label>Device Command Channels</label>
+ <description>sent commands to the device manually</description>
+ <channels>
+ <channel id="method" typeId="deviceCommand"/>
+ <channel id="params" typeId="deviceParams"/>
+ <channel id="secure" typeId="securePasstrough"/>
+ <channel id="send" typeId="sendSwitch"/>
</channels>
</channel-group-type>
<!-- ############################### CHANNELS ############################### -->
- <!-- OuputState Channel Type -->
+ <!-- response from device -->
<channel-type id="deviceResponse">
<item-type>String</item-type>
<label>Device Response</label>
- <description>DeviceResponse</description>
- <state readOnly="true"/>
+ <description>DeviceResponse from device.</description>
+ <state readOnly="false"/>
</channel-type>
- <!-- OuputState Channel Type -->
+ <!-- device command -->
<channel-type id="deviceCommand">
<item-type>String</item-type>
<label>Device Command</label>
- <description>command send to device. use: 'command':'value'</description>
+ <description>command (method) to send to device</description>
+ <state readOnly="false">
+ <options>
+ <option value="setDeviceData">Set Data</option>
+ <option value="queryInfo">Query DeviceInfo</option>
+ <option value="getChildData">Get Child Data</option>
+ </options>
+ </state>
+ </channel-type>
+
+ <!-- device params -->
+ <channel-type id="deviceParams">
+ <item-type>String</item-type>
+ <label>Params</label>
+ <description>params to send to device. Use JSON String</description>
+ <state readOnly="false"/>
+ </channel-type>
+
+ <!-- secure passtrough -->
+ <channel-type id="securePasstrough">
+ <item-type>Switch</item-type>
+ <label>Encrypt Message</label>
+ <description>encrypt message when sent to device</description>
<state readOnly="false"/>
</channel-type>
+ <!-- secure passtrough -->
+ <channel-type id="sendSwitch">
+ <item-type>Switch</item-type>
+ <label>Send</label>
+ <description>send command to device</description>
+ <state readOnly="false"/>
+ </channel-type>
+
+
</thing:thing-descriptions>