Merge PR #164: New Plugin: Tuya cloud
commit
359c6c98ed
|
|
@ -564,6 +564,36 @@ Description: nymea.io plugin for Texas Instruments devices
|
|||
This package will install the nymea.io plugin for Texas Instruments devices
|
||||
|
||||
|
||||
Package: nymea-plugin-tplink
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends},
|
||||
${misc:Depends},
|
||||
nymea-plugins-translations,
|
||||
Description: nymea.io plugin for tp-link Kasa devices
|
||||
The nymea daemon is a plugin based IoT (Internet of Things) server. The
|
||||
server works like a translator for devices, things and services and
|
||||
allows them to interact.
|
||||
With the powerful rule engine you are able to connect any device available
|
||||
in the system and create individual scenes and behaviors for your environment.
|
||||
.
|
||||
This package will install the nymea.io plugin for tp-link Kasa devices
|
||||
|
||||
|
||||
Package: nymea-plugin-tuya
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends},
|
||||
${misc:Depends},
|
||||
nymea-plugins-translations,
|
||||
Description: nymea.io plugin for Tuya cloud devices
|
||||
The nymea daemon is a plugin based IoT (Internet of Things) server. The
|
||||
server works like a translator for devices, things and services and
|
||||
allows them to interact.
|
||||
With the powerful rule engine you are able to connect any device available
|
||||
in the system and create individual scenes and behaviors for your environment.
|
||||
.
|
||||
This package will install the nymea.io plugin for Tuya cloud devices
|
||||
|
||||
|
||||
Package: nymea-plugin-udpcommander
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends},
|
||||
|
|
@ -852,6 +882,7 @@ Depends: nymea-plugin-anel,
|
|||
nymea-plugin-pushbullet,
|
||||
nymea-plugin-wakeonlan,
|
||||
nymea-plugin-tasmota,
|
||||
nymea-plugin-tplink,
|
||||
nymea-plugin-wemo,
|
||||
nymea-plugin-elgato,
|
||||
nymea-plugin-shelly,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugintplink.so
|
||||
|
|
@ -0,0 +1 @@
|
|||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugintuya.so
|
||||
|
|
@ -46,6 +46,8 @@ PLUGIN_DIRS = \
|
|||
tasmota \
|
||||
tcpcommander \
|
||||
texasinstruments \
|
||||
tplink \
|
||||
tuya \
|
||||
udpcommander \
|
||||
unitec \
|
||||
wakeonlan \
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# tp-link Kasa
|
||||
|
||||
This plugin adds support for tp-link Kasa smart plugs to nymea. Supported features are controlling power
|
||||
and reading energy consumption.
|
||||
|
||||
In order to use such a device, it must be connected to the same network as nymea. The Kasa app is required
|
||||
for a one time setup of the device to connect it to the Wi-Fi.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,434 @@
|
|||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2020 Michael Zanetti <michael.zanetti@nymea.io> *
|
||||
* *
|
||||
* This file is part of nymea. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Lesser General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2.1 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with this library; If not, see *
|
||||
* <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "deviceplugintplink.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <network/networkaccessmanager.h>
|
||||
#include <plugintimer.h>
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QTimer>
|
||||
#include <QDataStream>
|
||||
|
||||
// https://github.com/softScheck/tplink-smartplug/blob/master/tplink-smarthome-commands.txt
|
||||
|
||||
DevicePluginTPLink::DevicePluginTPLink()
|
||||
{
|
||||
}
|
||||
|
||||
DevicePluginTPLink::~DevicePluginTPLink()
|
||||
{
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::init()
|
||||
{
|
||||
m_broadcastSocket = new QUdpSocket(this);
|
||||
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::discoverDevices(DeviceDiscoveryInfo *info)
|
||||
{
|
||||
QVariantMap map;
|
||||
QVariantMap getSysInfo;
|
||||
getSysInfo.insert("get_sysinfo", QVariant());
|
||||
map.insert("system", getSysInfo);
|
||||
QByteArray payload = QJsonDocument::fromVariant(map).toJson(QJsonDocument::Compact);
|
||||
QByteArray datagram = encryptPayload(payload);
|
||||
|
||||
qint64 len = m_broadcastSocket->writeDatagram(datagram, QHostAddress::Broadcast, 9999);
|
||||
if (len != datagram.length()) {
|
||||
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("An error happened sending the discovery to the network."));
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(2000, info, [this, info](){
|
||||
while(m_broadcastSocket->hasPendingDatagrams()) {
|
||||
char buffer[1024];
|
||||
QHostAddress senderAddress;
|
||||
qint64 len = m_broadcastSocket->readDatagram(buffer, 1024, &senderAddress);
|
||||
QByteArray data = decryptPayload(QByteArray::fromRawData(buffer, len));
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTplink()) << "Error parsing JSON from device:" << data;
|
||||
continue;
|
||||
}
|
||||
QVariantMap properties = jsonDoc.toVariant().toMap();
|
||||
QVariantMap sysInfo = properties.value("system").toMap().value("get_sysinfo").toMap();
|
||||
if (sysInfo.value("type").toString() == "IOT.SMARTPLUGSWITCH") {
|
||||
|
||||
DeviceDescriptor descriptor(kasaPlugDeviceClassId, sysInfo.value("alias").toString(), sysInfo.value("dev_name").toString());
|
||||
Param idParam = Param(kasaPlugDeviceIdParamTypeId, sysInfo.value("deviceId").toString());
|
||||
descriptor.setParams(ParamList() << idParam);
|
||||
Device *existingDevice = myDevices().findByParams(ParamList() << idParam);
|
||||
if (existingDevice) {
|
||||
descriptor.setDeviceId(existingDevice->id());
|
||||
}
|
||||
info->addDeviceDescriptor(descriptor);
|
||||
|
||||
} else {
|
||||
qCWarning(dcTplink()) << "Unhandled device type:" << sysInfo.value("type").toString();
|
||||
}
|
||||
|
||||
}
|
||||
info->finish(Device::DeviceErrorNoError);
|
||||
});
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::setupDevice(DeviceSetupInfo *info)
|
||||
{
|
||||
QVariantMap map;
|
||||
QVariantMap getSysInfo;
|
||||
getSysInfo.insert("get_sysinfo", QVariant());
|
||||
map.insert("system", getSysInfo);
|
||||
QVariantMap getRealTime;
|
||||
getRealTime.insert("get_realtime", QVariant());
|
||||
map.insert("emeter", getRealTime);
|
||||
QByteArray payload = QJsonDocument::fromVariant(map).toJson(QJsonDocument::Compact);
|
||||
QByteArray datagram = encryptPayload(payload);
|
||||
|
||||
qint64 len = m_broadcastSocket->writeDatagram(datagram, QHostAddress::Broadcast, 9999);
|
||||
if (len != datagram.length()) {
|
||||
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("An error happened finding the device in the network."));
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(2000, info, [this, info](){
|
||||
|
||||
while(m_broadcastSocket->hasPendingDatagrams()) {
|
||||
char buffer[1024];
|
||||
QHostAddress senderAddress;
|
||||
qint64 len = m_broadcastSocket->readDatagram(buffer, 1024, &senderAddress);
|
||||
QByteArray data = decryptPayload(QByteArray::fromRawData(buffer, len));
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTplink()) << "Error parsing JSON from device:" << data;
|
||||
continue;
|
||||
}
|
||||
QVariantMap properties = jsonDoc.toVariant().toMap();
|
||||
QVariantMap sysInfo = properties.value("system").toMap().value("get_sysinfo").toMap();
|
||||
if (info->device()->paramValue(kasaPlugDeviceIdParamTypeId).toString() == sysInfo.value("deviceId").toString()) {
|
||||
qCDebug(dcTplink()) << "Found device at" << senderAddress;
|
||||
|
||||
connectToDevice(info->device(), senderAddress);
|
||||
|
||||
info->finish(Device::DeviceErrorNoError);
|
||||
m_setupRetries.remove(info);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_setupRetries.contains(info) || m_setupRetries.value(info) < 5) {
|
||||
qCDebug(dcTplink()) << "Device not found in network. Retrying... (" << m_setupRetries[info] << ")";
|
||||
m_setupRetries[info]++;
|
||||
setupDevice(info);
|
||||
return;
|
||||
}
|
||||
m_setupRetries.remove(info);
|
||||
info->finish(Device::DeviceErrorDeviceNotFound, QT_TR_NOOP("The device could not be found on the network."));
|
||||
});
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::postSetupDevice(Device *device)
|
||||
{
|
||||
connect(device, &Device::nameChanged, this, [this, device](){
|
||||
QVariantMap map;
|
||||
QVariantMap systemMap;
|
||||
QVariantMap aliasMap;
|
||||
aliasMap.insert("alias", device->name());
|
||||
systemMap.insert("set_dev_alias", aliasMap);
|
||||
map.insert("system", systemMap);
|
||||
QByteArray payload = QJsonDocument::fromVariant(map).toJson(QJsonDocument::Compact);
|
||||
qCDebug(dcTplink) << "Setting device name:" << payload;
|
||||
payload = encryptPayload(payload);
|
||||
QByteArray data;
|
||||
QDataStream stream(&data, QIODevice::ReadWrite);
|
||||
stream << static_cast<quint32>(payload.length());
|
||||
data.append(payload);
|
||||
|
||||
Job job;
|
||||
job.id = m_jobIdx++;
|
||||
job.data = data;
|
||||
m_jobQueue[device].append(job);
|
||||
processQueue(device);
|
||||
});
|
||||
|
||||
if (!m_timer) {
|
||||
m_timer = hardwareManager()->pluginTimerManager()->registerTimer(1);
|
||||
connect(m_timer, &PluginTimer::timeout, this, [this](){
|
||||
foreach (Device *d, myDevices()) {
|
||||
if (!m_pendingJobs.contains(d) && m_jobQueue[d].isEmpty()) {
|
||||
fetchState(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::deviceRemoved(Device *device)
|
||||
{
|
||||
qCDebug(dcTplink()) << "Device removed" << device->name();
|
||||
m_sockets.remove(device);
|
||||
m_pendingJobs.remove(device);
|
||||
m_jobQueue.remove(device);
|
||||
|
||||
if (myDevices().isEmpty() && m_timer) {
|
||||
hardwareManager()->pluginTimerManager()->unregisterTimer(m_timer);
|
||||
m_timer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::executeAction(DeviceActionInfo *info)
|
||||
{
|
||||
QVariantMap map;
|
||||
QVariantMap systemMap;
|
||||
QVariantMap powerMap;
|
||||
powerMap.insert("state", info->action().param(kasaPlugPowerActionPowerParamTypeId).value().toBool() ? 1 : 0);
|
||||
systemMap.insert("set_relay_state", powerMap);
|
||||
map.insert("system", systemMap);
|
||||
|
||||
// qCDebug(dcTplink()) << "Executing action" << qUtf8Printable(QJsonDocument::fromVariant(map).toJson(QJsonDocument::Compact));
|
||||
|
||||
QByteArray payload = encryptPayload(QJsonDocument::fromVariant(map).toJson(QJsonDocument::Compact));
|
||||
QByteArray data;
|
||||
QDataStream stream(&data, QIODevice::ReadWrite);
|
||||
stream << static_cast<quint32>(payload.length());
|
||||
data.append(payload);
|
||||
|
||||
Job job;
|
||||
job.id = m_jobIdx++;
|
||||
job.data = data;
|
||||
job.actionInfo = info;
|
||||
m_jobQueue[info->device()].append(job);
|
||||
connect(info, &DeviceActionInfo::aborted, this, [=](){
|
||||
m_jobQueue[info->device()].removeAll(job);
|
||||
});
|
||||
|
||||
// Directly queue up fetchState
|
||||
fetchState(info->device(), info);
|
||||
|
||||
processQueue(info->device());
|
||||
}
|
||||
|
||||
QByteArray DevicePluginTPLink::encryptPayload(const QByteArray &payload)
|
||||
{
|
||||
QByteArray result;
|
||||
int k = 171;
|
||||
for (int i = 0; i < payload.length(); i++){
|
||||
char t = payload.at(i) xor k;
|
||||
k = t;
|
||||
result.append(t);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray DevicePluginTPLink::decryptPayload(const QByteArray &payload)
|
||||
{
|
||||
QByteArray result;
|
||||
int k = 171;
|
||||
for (int i = 0; i < payload.length(); i++){
|
||||
char t = payload.at(i);
|
||||
result.append(t xor k);
|
||||
k = t;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::connectToDevice(Device *device, const QHostAddress &address)
|
||||
{
|
||||
if (m_sockets.contains(device)) {
|
||||
qCWarning(dcTplink) << "Already have a connection to this device";
|
||||
return;
|
||||
}
|
||||
qCDebug(dcTplink()) << "Connecting to" << address;
|
||||
|
||||
QTcpSocket *socket = new QTcpSocket(this);
|
||||
m_sockets.insert(device, socket);
|
||||
|
||||
connect(socket, &QTcpSocket::connected, device, [this, device, address] () {
|
||||
qCDebug(dcTplink()) << "Connected to device" << address;
|
||||
device->setStateValue(kasaPlugConnectedStateTypeId, true);
|
||||
fetchState(device);
|
||||
});
|
||||
|
||||
typedef void (QTcpSocket:: *errorSignal)(QAbstractSocket::SocketError);
|
||||
connect(socket, static_cast<errorSignal>(&QTcpSocket::error), device, [](QAbstractSocket::SocketError error) {
|
||||
qCWarning(dcTplink()) << "Error in device connection:" << error;
|
||||
});
|
||||
|
||||
connect(socket, &QTcpSocket::readyRead, device, [this, socket, device](){
|
||||
m_inputBuffers[device].append(socket->readAll());
|
||||
while (m_inputBuffers[device].length() > 4) {
|
||||
QByteArray data = m_inputBuffers[device];
|
||||
QDataStream stream(data);
|
||||
qint32 len;
|
||||
stream >> len;
|
||||
data.remove(0, 4);
|
||||
if (data.length() < len) {
|
||||
// Buffer not complete... wait for more...
|
||||
return;
|
||||
}
|
||||
QByteArray payload = data.left(len);
|
||||
data.remove(0, len);
|
||||
m_inputBuffers[device] = data;
|
||||
|
||||
if (!m_pendingJobs.contains(device)) {
|
||||
qCWarning(dcTplink()) << "Received packet from device but don't have a job waiting for it. Did it time out?";
|
||||
processQueue(device);
|
||||
return;
|
||||
}
|
||||
Job job = m_pendingJobs.take(device);
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(decryptPayload(payload), &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTplink()) << "Cannot parse json from device:" << decryptPayload(payload);
|
||||
m_jobQueue[device].prepend(job);
|
||||
socket->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
// qCDebug(dcTplink()) << "Socket data received" << qUtf8Printable(jsonDoc.toJson());
|
||||
|
||||
QVariantMap map = jsonDoc.toVariant().toMap();
|
||||
if (map.contains("system")) {
|
||||
QVariantMap systemMap = map.value("system").toMap();
|
||||
if (systemMap.contains("set_relay_state")) {
|
||||
int err_code = systemMap.value("set_relay_state").toMap().value("err_code").toInt();
|
||||
if (err_code != 0) {
|
||||
qCWarning(dcTplink()) << "Set relay state failed:" << qUtf8Printable(jsonDoc.toJson());
|
||||
if (job.actionInfo) {
|
||||
job.actionInfo->finish(Device::DeviceErrorHardwareFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (systemMap.contains("get_sysinfo")) {
|
||||
int relayState = systemMap.value("get_sysinfo").toMap().value("relay_state").toInt();
|
||||
device->setStateValue(kasaPlugPowerStateTypeId, relayState == 1 ? true : false);
|
||||
|
||||
QString alias = systemMap.value("get_sysinfo").toMap().value("alias").toString();
|
||||
if (device->name() != alias) {
|
||||
device->setName(alias);
|
||||
}
|
||||
|
||||
if (job.actionInfo) {
|
||||
job.actionInfo->finish(Device::DeviceErrorNoError);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (map.contains("emeter")) {
|
||||
QVariantMap emeterMap = map.value("emeter").toMap();
|
||||
if (emeterMap.contains("get_realtime")) {
|
||||
// This has quite a bit of jitter... Let's smoothen it while within +/- 0.1W to produce less events in the system
|
||||
double oldValue = device->stateValue(kasaPlugCurrentPowerStateTypeId).toDouble();
|
||||
double newValue = emeterMap.value("get_realtime").toMap().value("power_mw").toDouble() / 1000;
|
||||
qCDebug(dcTplink()) << "old:" << oldValue << "new" << newValue << "diff" << qAbs(oldValue - newValue);
|
||||
if (qAbs(oldValue - newValue) > 0.1) {
|
||||
device->setStateValue(kasaPlugCurrentPowerStateTypeId, newValue);
|
||||
}
|
||||
device->setStateValue(kasaPlugTotalEnergyConsumedStateTypeId, emeterMap.value("get_realtime").toMap().value("total_wh").toDouble() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
processQueue(device);
|
||||
}
|
||||
});
|
||||
|
||||
connect(socket, &QTcpSocket::disconnected, device, [this, device, address](){
|
||||
qCDebug(dcTplink()) << "Device disconnected";
|
||||
m_sockets.take(device)->deleteLater();
|
||||
if (m_pendingJobs.contains(device)) {
|
||||
// Putting active job back to queue
|
||||
m_jobQueue[device].prepend(m_pendingJobs.take(device));
|
||||
}
|
||||
device->setStateValue(kasaPlugConnectedStateTypeId, false);
|
||||
QTimer::singleShot(500, device, [this, device, address]() {connectToDevice(device, address);});
|
||||
});
|
||||
|
||||
socket->connectToHost(address.toString(), 9999, QIODevice::ReadWrite);
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::fetchState(Device *device, DeviceActionInfo *info)
|
||||
{
|
||||
QTcpSocket *socket = m_sockets.value(device);
|
||||
if (!socket || !socket->isOpen()) {
|
||||
qCWarning(dcTplink()) << "Cannot fetch state";
|
||||
}
|
||||
|
||||
QVariantMap map;
|
||||
QVariantMap getSysInfo;
|
||||
getSysInfo.insert("get_sysinfo", QVariant());
|
||||
map.insert("system", getSysInfo);
|
||||
QVariantMap getRealTime;
|
||||
getRealTime.insert("get_realtime", QVariant());
|
||||
map.insert("emeter", getRealTime);
|
||||
QByteArray plaintext = QJsonDocument::fromVariant(map).toJson(QJsonDocument::Compact);
|
||||
// qCDebug(dcTplink()) << "Fetching device state";
|
||||
QByteArray payload = encryptPayload(plaintext);
|
||||
QByteArray data;
|
||||
QDataStream stream(&data, QIODevice::ReadWrite);
|
||||
stream << static_cast<quint32>(payload.length());
|
||||
data.append(payload);
|
||||
|
||||
Job job;
|
||||
job.id = m_jobIdx++;
|
||||
job.data = data;
|
||||
job.actionInfo = info;
|
||||
m_jobQueue[device].append(job);
|
||||
|
||||
processQueue(device);
|
||||
|
||||
}
|
||||
|
||||
void DevicePluginTPLink::processQueue(Device *device)
|
||||
{
|
||||
if (m_pendingJobs.contains(device)) {
|
||||
// Busy
|
||||
return;
|
||||
}
|
||||
if (m_jobQueue[device].isEmpty()) {
|
||||
// No jobs queued for this device
|
||||
return;
|
||||
}
|
||||
|
||||
QTcpSocket *socket = m_sockets.value(device);
|
||||
if (!socket) {
|
||||
qCWarning(dcTplink()) << "Cannot process queue. Device not connected.";
|
||||
return;
|
||||
}
|
||||
Job job = m_jobQueue[device].takeFirst();
|
||||
|
||||
m_pendingJobs[device] = job;
|
||||
|
||||
qint64 len = socket->write(job.data);
|
||||
if (len != job.data.length()) {
|
||||
qCWarning(dcTplink()) << "Error writing data to network.";
|
||||
if (job.actionInfo) {
|
||||
job.actionInfo->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Error sending command to the network."));
|
||||
}
|
||||
socket->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2020 Michael Zanetti <michael.zanetti@nymea.io> *
|
||||
* *
|
||||
* This file is part of nymea. *
|
||||
* *
|
||||
* nymea is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, version 2 of the License. *
|
||||
* *
|
||||
* nymea is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with nymea. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef DEVICEPLUGINTPLINK_H
|
||||
#define DEVICEPLUGINTPLINK_H
|
||||
|
||||
#include "devices/deviceplugin.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class PluginTimer;
|
||||
|
||||
class DevicePluginTPLink: public DevicePlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "deviceplugintplink.json")
|
||||
Q_INTERFACES(DevicePlugin)
|
||||
|
||||
public:
|
||||
explicit DevicePluginTPLink();
|
||||
~DevicePluginTPLink();
|
||||
|
||||
void init() override;
|
||||
void discoverDevices(DeviceDiscoveryInfo *info) override;
|
||||
void setupDevice(DeviceSetupInfo *info) override;
|
||||
void postSetupDevice(Device *device) override;
|
||||
void deviceRemoved(Device *device) override;
|
||||
void executeAction(DeviceActionInfo *info) override;
|
||||
|
||||
private:
|
||||
QByteArray encryptPayload(const QByteArray &payload);
|
||||
QByteArray decryptPayload(const QByteArray &payload);
|
||||
|
||||
void connectToDevice(Device *device, const QHostAddress &address);
|
||||
void fetchState(Device *device, DeviceActionInfo *info = nullptr);
|
||||
|
||||
void processQueue(Device *device);
|
||||
|
||||
private:
|
||||
class Job {
|
||||
public:
|
||||
int id = 0;
|
||||
QByteArray data;
|
||||
DeviceActionInfo *actionInfo = nullptr;
|
||||
bool operator==(const Job &other) { return id == other.id; }
|
||||
};
|
||||
QHash<Device*, Job> m_pendingJobs;
|
||||
QHash<Device*, QList<Job>> m_jobQueue;
|
||||
int m_jobIdx = 0;
|
||||
|
||||
QUdpSocket *m_broadcastSocket = nullptr;
|
||||
QHash<Device*, QTcpSocket*> m_sockets;
|
||||
QHash<DeviceSetupInfo*, int> m_setupRetries;
|
||||
|
||||
QHash<Device*, QByteArray> m_inputBuffers;
|
||||
|
||||
PluginTimer *m_timer = nullptr;
|
||||
};
|
||||
|
||||
#endif // DEVICEPLUGINANEL_H
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "tplink",
|
||||
"displayName": "tp-link",
|
||||
"id": "024ff2e3-30df-44a1-9c8d-63cc416f1fb8",
|
||||
"vendors": [
|
||||
{
|
||||
"name": "tplink",
|
||||
"displayName": "tp-link",
|
||||
"id": "8603b6cf-52ec-4481-aca2-f29ebd6cd8a8",
|
||||
"deviceClasses": [
|
||||
{
|
||||
"id": "32830124-9efb-4614-8227-ee269b1889b0",
|
||||
"name": "kasaPlug",
|
||||
"displayName": "Kasa Smart Wi-Fi Plug",
|
||||
"createMethods": ["discovery"],
|
||||
"interfaces": [ "powersocket", "extendedsmartmeterconsumer", "connectable" ],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "de3238f7-fe94-440d-b212-61cd4e221b50",
|
||||
"name": "id",
|
||||
"displayName": "ID",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "b66825ec-9f1b-48da-af18-f36913291c0e",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"displayNameEvent": "Connected changed",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "f1a5fda4-87a6-46f6-9499-16811a5f4f4d",
|
||||
"name": "power",
|
||||
"displayName": "Power",
|
||||
"displayNameEvent": "Turned on or off",
|
||||
"displayNameAction": "Turn on or off",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "a3533121-69ee-44fd-8394-13373e8f960e",
|
||||
"name": "totalEnergyConsumed",
|
||||
"displayName": "Total energy consumed",
|
||||
"displayNameEvent": "Total energy consumed changed",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "ccb52b57-5800-4f03-b7fa-f36dcebe1d4e",
|
||||
"name": "currentPower",
|
||||
"displayName": "Current power consumption",
|
||||
"displayNameEvent": "Current power consumption changed",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
include(../plugins.pri)
|
||||
|
||||
QT += network
|
||||
|
||||
SOURCES += \
|
||||
deviceplugintplink.cpp \
|
||||
|
||||
HEADERS += \
|
||||
deviceplugintplink.h \
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="de">
|
||||
<context>
|
||||
<name>DevicePluginTPLink</name>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="60"/>
|
||||
<source>An error happened sending the discovery to the network.</source>
|
||||
<translation>Beim Durchsuchen des Netzwerks ist ein Fehler aufgetreten.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="107"/>
|
||||
<source>An error happened finding the device in the network.</source>
|
||||
<translation>Beim Suchen des Geräts ist ein Fehler aufgetreten.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="144"/>
|
||||
<source>The device could not be found on the network.</source>
|
||||
<translation>Das Gerät konnte nicht im Netzwerk gefunden werden.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="412"/>
|
||||
<source>Error sending command to the network.</source>
|
||||
<translation>Der Befehl konnte nicht ins Netzwerk gesendet werden.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>tplink</name>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="38"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="41"/>
|
||||
<source>Connected</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, EventType: connected, ID: {b66825ec-9f1b-48da-af18-f36913291c0e})
|
||||
----------
|
||||
The name of the StateType ({b66825ec-9f1b-48da-af18-f36913291c0e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Verbunden</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="44"/>
|
||||
<source>Connected changed</source>
|
||||
<extracomment>The name of the EventType ({b66825ec-9f1b-48da-af18-f36913291c0e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Verbunden/getrennt</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="47"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="50"/>
|
||||
<source>Current power consumption</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, EventType: currentPower, ID: {ccb52b57-5800-4f03-b7fa-f36dcebe1d4e})
|
||||
----------
|
||||
The name of the StateType ({ccb52b57-5800-4f03-b7fa-f36dcebe1d4e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Aktueller Energieverbrauch</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="53"/>
|
||||
<source>Current power consumption changed</source>
|
||||
<extracomment>The name of the EventType ({ccb52b57-5800-4f03-b7fa-f36dcebe1d4e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Aktueller Energieverbrauch geändert</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="56"/>
|
||||
<source>ID</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, Type: device, ID: {de3238f7-fe94-440d-b212-61cd4e221b50})</extracomment>
|
||||
<translation>ID</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="59"/>
|
||||
<source>Kasa Smart Wi-Fi Plug</source>
|
||||
<extracomment>The name of the DeviceClass ({32830124-9efb-4614-8227-ee269b1889b0})</extracomment>
|
||||
<translation>Kasa Smart Wi-Fi Plug</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="62"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="65"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="68"/>
|
||||
<source>Power</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, ActionType: power, ID: {f1a5fda4-87a6-46f6-9499-16811a5f4f4d})
|
||||
----------
|
||||
The name of the ParamType (DeviceClass: kasaPlug, EventType: power, ID: {f1a5fda4-87a6-46f6-9499-16811a5f4f4d})
|
||||
----------
|
||||
The name of the StateType ({f1a5fda4-87a6-46f6-9499-16811a5f4f4d}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Eingeschaltet</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="71"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="74"/>
|
||||
<source>Total energy consumed</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, EventType: totalEnergyConsumed, ID: {a3533121-69ee-44fd-8394-13373e8f960e})
|
||||
----------
|
||||
The name of the StateType ({a3533121-69ee-44fd-8394-13373e8f960e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Gesamter Energieverbrauch</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="77"/>
|
||||
<source>Total energy consumed changed</source>
|
||||
<extracomment>The name of the EventType ({a3533121-69ee-44fd-8394-13373e8f960e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Gesamter Energieverbrauch geändert</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="80"/>
|
||||
<source>Turn on or off</source>
|
||||
<extracomment>The name of the ActionType ({f1a5fda4-87a6-46f6-9499-16811a5f4f4d}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Ein- ausschalten</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="83"/>
|
||||
<source>Turned on or off</source>
|
||||
<extracomment>The name of the EventType ({f1a5fda4-87a6-46f6-9499-16811a5f4f4d}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation>Ein- ausgeschaltet</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="86"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="89"/>
|
||||
<source>tp-link</source>
|
||||
<extracomment>The name of the vendor ({8603b6cf-52ec-4481-aca2-f29ebd6cd8a8})
|
||||
----------
|
||||
The name of the plugin tplink ({024ff2e3-30df-44a1-9c8d-63cc416f1fb8})</extracomment>
|
||||
<translation>tp-link</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1">
|
||||
<context>
|
||||
<name>DevicePluginTPLink</name>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="60"/>
|
||||
<source>An error happened sending the discovery to the network.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="107"/>
|
||||
<source>An error happened finding the device in the network.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="144"/>
|
||||
<source>The device could not be found on the network.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../deviceplugintplink.cpp" line="412"/>
|
||||
<source>Error sending command to the network.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>tplink</name>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="38"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="41"/>
|
||||
<source>Connected</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, EventType: connected, ID: {b66825ec-9f1b-48da-af18-f36913291c0e})
|
||||
----------
|
||||
The name of the StateType ({b66825ec-9f1b-48da-af18-f36913291c0e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="44"/>
|
||||
<source>Connected changed</source>
|
||||
<extracomment>The name of the EventType ({b66825ec-9f1b-48da-af18-f36913291c0e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="47"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="50"/>
|
||||
<source>Current power consumption</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, EventType: currentPower, ID: {ccb52b57-5800-4f03-b7fa-f36dcebe1d4e})
|
||||
----------
|
||||
The name of the StateType ({ccb52b57-5800-4f03-b7fa-f36dcebe1d4e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="53"/>
|
||||
<source>Current power consumption changed</source>
|
||||
<extracomment>The name of the EventType ({ccb52b57-5800-4f03-b7fa-f36dcebe1d4e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="56"/>
|
||||
<source>ID</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, Type: device, ID: {de3238f7-fe94-440d-b212-61cd4e221b50})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="59"/>
|
||||
<source>Kasa Smart Wi-Fi Plug</source>
|
||||
<extracomment>The name of the DeviceClass ({32830124-9efb-4614-8227-ee269b1889b0})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="62"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="65"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="68"/>
|
||||
<source>Power</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, ActionType: power, ID: {f1a5fda4-87a6-46f6-9499-16811a5f4f4d})
|
||||
----------
|
||||
The name of the ParamType (DeviceClass: kasaPlug, EventType: power, ID: {f1a5fda4-87a6-46f6-9499-16811a5f4f4d})
|
||||
----------
|
||||
The name of the StateType ({f1a5fda4-87a6-46f6-9499-16811a5f4f4d}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="71"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="74"/>
|
||||
<source>Total energy consumed</source>
|
||||
<extracomment>The name of the ParamType (DeviceClass: kasaPlug, EventType: totalEnergyConsumed, ID: {a3533121-69ee-44fd-8394-13373e8f960e})
|
||||
----------
|
||||
The name of the StateType ({a3533121-69ee-44fd-8394-13373e8f960e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="77"/>
|
||||
<source>Total energy consumed changed</source>
|
||||
<extracomment>The name of the EventType ({a3533121-69ee-44fd-8394-13373e8f960e}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="80"/>
|
||||
<source>Turn on or off</source>
|
||||
<extracomment>The name of the ActionType ({f1a5fda4-87a6-46f6-9499-16811a5f4f4d}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="83"/>
|
||||
<source>Turned on or off</source>
|
||||
<extracomment>The name of the EventType ({f1a5fda4-87a6-46f6-9499-16811a5f4f4d}) of DeviceClass kasaPlug</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="86"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tplink/plugininfo.h" line="89"/>
|
||||
<source>tp-link</source>
|
||||
<extracomment>The name of the vendor ({8603b6cf-52ec-4481-aca2-f29ebd6cd8a8})
|
||||
----------
|
||||
The name of the plugin tplink ({024ff2e3-30df-44a1-9c8d-63cc416f1fb8})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Tuya
|
||||
|
||||
This plugin allows to make use of Tuya based devices through the Tuya cloud. This includes all the devices that work with the Smart Life app.
|
||||
|
||||
The plugin will allow logging in with the Smart Life app account and fetch all the devices connected to the Tuya/Smart Life cloud.
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2019 Michael Zanetti <michael.zanetti@nymea.io> *
|
||||
* *
|
||||
* This file is part of nymea. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Lesser General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2.1 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
* Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public *
|
||||
* License along with this library; If not, see *
|
||||
* <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "deviceplugintuya.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QColor>
|
||||
|
||||
#include "hardwaremanager.h"
|
||||
#include "network/networkaccessmanager.h"
|
||||
|
||||
#include "plugintimer.h"
|
||||
|
||||
// API info:
|
||||
// Python project: https://github.com/PaulAnnekov/tuyaha
|
||||
// JS project: https://github.com/unparagoned/cloudtuya
|
||||
|
||||
DevicePluginTuya::DevicePluginTuya(QObject *parent): DevicePlugin(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
DevicePluginTuya::~DevicePluginTuya()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void DevicePluginTuya::setupDevice(DeviceSetupInfo *info)
|
||||
{
|
||||
Device *device = info->device();
|
||||
if (device->deviceClassId() == tuyaCloudDeviceClassId) {
|
||||
QTimer *tokenRefreshTimer = m_tokenExpiryTimers.value(device->id());
|
||||
if (!tokenRefreshTimer) {
|
||||
tokenRefreshTimer = new QTimer(device);
|
||||
tokenRefreshTimer->setSingleShot(true);
|
||||
m_tokenExpiryTimers.insert(device->id(), tokenRefreshTimer);
|
||||
}
|
||||
|
||||
connect(tokenRefreshTimer, &QTimer::timeout, device, [this, device](){
|
||||
qCDebug(dcTuya()) << "Timer refresh token";
|
||||
refreshAccessToken(device);
|
||||
});
|
||||
|
||||
// If token refresh timer is already running, we just passed the login...
|
||||
if (tokenRefreshTimer->isActive()) {
|
||||
qCDebug(dcTuya()) << "Device already set up during pairing.";
|
||||
device->setStateValue(tuyaCloudConnectedStateTypeId, true);
|
||||
device->setStateValue(tuyaCloudLoggedInStateTypeId, true);
|
||||
pluginStorage()->beginGroup(device->id().toString());
|
||||
QString username = pluginStorage()->value("username").toString();
|
||||
pluginStorage()->endGroup();
|
||||
device->setStateValue(tuyaCloudUserDisplayNameStateTypeId, username);
|
||||
return info->finish(Device::DeviceErrorNoError);
|
||||
}
|
||||
|
||||
// Else, let's refresh the token now
|
||||
qCDebug(dcTuya()) << "Setup refresh token";
|
||||
refreshAccessToken(device);
|
||||
|
||||
connect(this, &DevicePluginTuya::tokenRefreshed, info, [info](Device *device, bool success){
|
||||
if (device == info->device()) {
|
||||
if (!success) {
|
||||
info->finish(Device::DeviceErrorAuthenticationFailure, QT_TR_NOOP("Error authenticating to Tuya device."));
|
||||
} else {
|
||||
info->finish(Device::DeviceErrorNoError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return ;
|
||||
}
|
||||
|
||||
info->finish(Device::DeviceErrorNoError);
|
||||
}
|
||||
|
||||
void DevicePluginTuya::postSetupDevice(Device *device)
|
||||
{
|
||||
if (device->deviceClassId() == tuyaCloudDeviceClassId) {
|
||||
updateChildDevices(device);
|
||||
|
||||
if (!m_pluginTimer) {
|
||||
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(5);
|
||||
connect(m_pluginTimer, &PluginTimer::timeout, this, [this](){
|
||||
foreach (Device *d, myDevices().filterByDeviceClassId(tuyaCloudDeviceClassId)) {
|
||||
updateChildDevices(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DevicePluginTuya::deviceRemoved(Device *device)
|
||||
{
|
||||
if (device->deviceClassId() == tuyaCloudDeviceClassId) {
|
||||
m_tokenExpiryTimers.take(device->id())->deleteLater();
|
||||
}
|
||||
|
||||
if (myDevices().isEmpty()) {
|
||||
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
|
||||
m_pluginTimer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void DevicePluginTuya::startPairing(DevicePairingInfo *info)
|
||||
{
|
||||
info->finish(Device::DeviceErrorNoError, QT_TR_NOOP("Please enter username and password for your Tuya (Smart Life) account."));
|
||||
}
|
||||
|
||||
void DevicePluginTuya::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret)
|
||||
{
|
||||
QUrl url(QString("http://px1.tuyaeu.com/homeassistant/auth.do"));
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("userName", username);
|
||||
query.addQueryItem("password", secret);
|
||||
query.addQueryItem("countryCode", "44");
|
||||
query.addQueryItem("bizType", "smart_life");
|
||||
query.addQueryItem("from", "tuya");
|
||||
|
||||
|
||||
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, query.toString().toUtf8());
|
||||
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
|
||||
|
||||
qCDebug(dcTuya()) << "Pairing Tuya device";
|
||||
connect(reply, &QNetworkReply::finished, info, [this, reply, info, username](){
|
||||
reply->deleteLater();
|
||||
QByteArray data = reply->readAll();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(dcTuya()) << "Server error:" << reply->errorString();
|
||||
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Error communicating with Tuya server."));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTuya()) << "Json parse error:" << error.errorString();
|
||||
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Error communicating with Tuya server."));
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(dcTuya()) << "Response from tuya api:" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
||||
|
||||
QVariantMap result = jsonDoc.toVariant().toMap();
|
||||
|
||||
|
||||
if (result.value("responseStatus") == "error") {
|
||||
qCDebug(dcTuya()) << "Error response from service.";
|
||||
info->finish(Device::DeviceErrorAuthenticationFailure, QT_TR_NOOP("Wrong username or password."));
|
||||
return;
|
||||
}
|
||||
|
||||
pluginStorage()->beginGroup(info->deviceId().toString());
|
||||
pluginStorage()->setValue("accessToken", result.value("access_token").toString());
|
||||
pluginStorage()->setValue("refreshToken", result.value("refresh_token").toString());
|
||||
pluginStorage()->setValue("username", username);
|
||||
pluginStorage()->endGroup();
|
||||
|
||||
int timeout = result.value("expires_in").toInt();
|
||||
|
||||
QTimer *t = new QTimer(this);
|
||||
t->setSingleShot(true);
|
||||
t->start(timeout * 1000);
|
||||
|
||||
m_tokenExpiryTimers.insert(info->deviceId(), t);
|
||||
|
||||
qCDebug(dcTuya()) << "Tuya device paired. Token expires in" << timeout;
|
||||
|
||||
info->finish(Device::DeviceErrorNoError);
|
||||
});
|
||||
}
|
||||
|
||||
void DevicePluginTuya::executeAction(DeviceActionInfo *info)
|
||||
{
|
||||
if (info->action().actionTypeId() == tuyaSwitchPowerActionTypeId) {
|
||||
bool on = info->action().param(tuyaSwitchPowerActionPowerParamTypeId).value().toBool();
|
||||
controlTuyaSwitch("turnOnOff", on ? "1" : "0", info);
|
||||
connect(info, &DeviceActionInfo::finished, [info, on](){
|
||||
info->device()->setStateValue(tuyaSwitchPowerStateTypeId, on);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (info->action().actionTypeId() == tuyaClosableOpenActionTypeId) {
|
||||
controlTuyaSwitch("turnOnOff", "1", info);
|
||||
return;
|
||||
}
|
||||
if (info->action().actionTypeId() == tuyaClosableCloseActionTypeId) {
|
||||
controlTuyaSwitch("turnOnOff", "0", info);
|
||||
return;
|
||||
}
|
||||
if (info->action().actionTypeId() == tuyaClosableStopActionTypeId) {
|
||||
controlTuyaSwitch("startStop", "0", info);
|
||||
return;
|
||||
}
|
||||
Q_ASSERT_X(false, "tuyaplugin", "Unhandled action type " + info->action().actionTypeId().toByteArray());
|
||||
}
|
||||
|
||||
void DevicePluginTuya::refreshAccessToken(Device *device)
|
||||
{
|
||||
qCDebug(dcTuya()) << device->name() << "Refreshing access token for" << device->name();
|
||||
|
||||
pluginStorage()->beginGroup(device->id().toString());
|
||||
QString refreshToken = pluginStorage()->value("refreshToken").toString();
|
||||
pluginStorage()->endGroup();
|
||||
|
||||
QUrl url("http://px1.tuyaeu.com/homeassistant/access.do");
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("grant_type", "refresh_token");
|
||||
query.addQueryItem("refresh_token", refreshToken);
|
||||
url.setQuery(query);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
|
||||
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
|
||||
connect(reply, &QNetworkReply::finished, [reply](){ reply->deleteLater(); });
|
||||
|
||||
connect(reply, &QNetworkReply::finished, device, [this, reply, device](){
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(dcTuya()) << "Error refreshing access token";
|
||||
device->setStateValue(tuyaCloudConnectedStateTypeId, false);
|
||||
device->setStateValue(tuyaCloudLoggedInStateTypeId, false);
|
||||
emit tokenRefreshed(device, false);
|
||||
return;
|
||||
}
|
||||
QByteArray data = reply->readAll();
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTuya()) << "Failed to parse json reply when refreshing access token" << error.errorString();
|
||||
device->setStateValue(tuyaCloudConnectedStateTypeId, false);
|
||||
device->setStateValue(tuyaCloudLoggedInStateTypeId, false);
|
||||
emit tokenRefreshed(device, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (jsonDoc.toVariant().toMap().isEmpty()) {
|
||||
qCWarning(dcTuya()) << "Empty response from Tuya server";
|
||||
device->setStateValue(tuyaCloudConnectedStateTypeId, false);
|
||||
device->setStateValue(tuyaCloudLoggedInStateTypeId, false);
|
||||
return;
|
||||
}
|
||||
|
||||
pluginStorage()->beginGroup(device->id().toString());
|
||||
pluginStorage()->setValue("accessToken", jsonDoc.toVariant().toMap().value("access_token").toString());
|
||||
pluginStorage()->setValue("refreshToken", jsonDoc.toVariant().toMap().value("refresh_token").toString());
|
||||
pluginStorage()->endGroup();
|
||||
int tokenExpiry = jsonDoc.toVariant().toMap().value("expires_in").toInt();
|
||||
|
||||
qCDebug(dcTuya()) << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
|
||||
qCDebug(dcTuya()) << "Access token for" << device->name() << "refreshed. Expires in" << tokenExpiry;
|
||||
|
||||
QTimer *t = m_tokenExpiryTimers.value(device->id());
|
||||
t->start(tokenExpiry);
|
||||
device->setStateValue(tuyaCloudConnectedStateTypeId, true);
|
||||
device->setStateValue(tuyaCloudLoggedInStateTypeId, true);
|
||||
|
||||
pluginStorage()->beginGroup(device->id().toString());
|
||||
QString username = pluginStorage()->value("username").toString();
|
||||
pluginStorage()->endGroup();
|
||||
device->setStateValue(tuyaCloudUserDisplayNameStateTypeId, username);
|
||||
|
||||
emit tokenRefreshed(device, true);
|
||||
});
|
||||
}
|
||||
|
||||
void DevicePluginTuya::updateChildDevices(Device *device)
|
||||
{
|
||||
qCDebug(dcTuya()) << device->name() << "Updating child devices";
|
||||
pluginStorage()->beginGroup(device->id().toString());
|
||||
QString accesToken = pluginStorage()->value("accessToken").toString();
|
||||
pluginStorage()->endGroup();
|
||||
|
||||
QUrl url("http://px1.tuyaeu.com/homeassistant/skill");
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QVariantMap header;
|
||||
header.insert("name", "Discovery");
|
||||
header.insert("namespace", "discovery");
|
||||
header.insert("payloadVersion", 1);
|
||||
|
||||
QVariantMap payload;
|
||||
payload.insert("accessToken", accesToken);
|
||||
|
||||
QVariantMap data;
|
||||
data.insert("header", header);
|
||||
data.insert("payload", payload);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(data);
|
||||
|
||||
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, jsonDoc.toJson(QJsonDocument::Compact));
|
||||
connect(reply, &QNetworkReply::finished, [reply](){reply->deleteLater();});
|
||||
connect(reply, &QNetworkReply::finished, device, [this, device, reply](){
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(dcTuya()) << "Error fetching devices from Tuya cloud" << reply->error();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTuya()) << "Json parser error updating child devices" << error.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap dataMap = jsonDoc.toVariant().toMap();
|
||||
if (!dataMap.contains("payload") || !dataMap.value("payload").toMap().contains("devices")) {
|
||||
qCWarning(dcTuya()) << "Invalid data from Tuya cloud:" << jsonDoc.toJson();
|
||||
return;
|
||||
}
|
||||
QVariantList devices = dataMap.value("payload").toMap().value("devices").toList();
|
||||
|
||||
qCDebug(dcTuya()) << "Devices fetched";
|
||||
|
||||
QList<DeviceDescriptor> unknownDevices;
|
||||
|
||||
foreach (const QVariant &deviceVariant, devices) {
|
||||
QVariantMap deviceMap = deviceVariant.toMap();
|
||||
QString devType = deviceMap.value("dev_type").toString();
|
||||
QString id = deviceMap.value("id").toString();
|
||||
QString name = deviceMap.value("name").toString();
|
||||
|
||||
if (devType == "switch") {
|
||||
bool online = deviceMap.value("data").toMap().value("online").toBool();
|
||||
bool state = deviceMap.value("data").toMap().value("state").toBool();
|
||||
|
||||
Device *d = myDevices().findByParams(ParamList() << Param(tuyaSwitchDeviceIdParamTypeId, id));
|
||||
if (d) {
|
||||
qCDebug(dcTuya()) << "Found existing Tuya switch" << d->name() << id << name << (online ? "online:" : "offline") << (state ? "on": "off");
|
||||
d->setStateValue(tuyaSwitchConnectedStateTypeId, online);
|
||||
d->setStateValue(tuyaSwitchPowerStateTypeId, state);
|
||||
} else {
|
||||
qCDebug(dcTuya()) << "Found new Tuya switch" << id << name;
|
||||
DeviceDescriptor descriptor(tuyaSwitchDeviceClassId, name, QString(), device->id());
|
||||
descriptor.setParams(ParamList() << Param(tuyaSwitchDeviceIdParamTypeId, id));
|
||||
unknownDevices.append(descriptor);
|
||||
}
|
||||
} else if (devType == "cover") {
|
||||
bool online = deviceMap.value("data").toMap().value("online").toBool();
|
||||
|
||||
Device *d = myDevices().findByParams(ParamList() << Param(tuyaClosableDeviceIdParamTypeId, id));
|
||||
if (d) {
|
||||
qCDebug(dcTuya()) << "Found existing Tuya cover" << d->name() << id << name << (online ? "online" : "offline");
|
||||
d->setStateValue(tuyaClosableConnectedStateTypeId, online);
|
||||
} else {
|
||||
qCDebug(dcTuya()) << "Found new Tuya cover" << id << name;
|
||||
DeviceDescriptor descriptor(tuyaClosableDeviceClassId, name, QString(), device->id());
|
||||
descriptor.setParams(ParamList() << Param(tuyaClosableDeviceIdParamTypeId, id));
|
||||
unknownDevices.append(descriptor);
|
||||
}
|
||||
} else {
|
||||
qCWarning(dcTuya()) << "Skipping unsupported device type:" << devType;
|
||||
qCWarning(dcTuya()) << "Please report this including the following data:\n" << qUtf8Printable(QJsonDocument::fromVariant(deviceVariant).toJson());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unknownDevices.isEmpty()) {
|
||||
emit autoDevicesAppeared(unknownDevices);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void DevicePluginTuya::controlTuyaSwitch(const QString &command, const QString &value, DeviceActionInfo *info)
|
||||
{
|
||||
Device *device = info->device();
|
||||
Device *parentDevice = myDevices().findById(device->parentId());
|
||||
|
||||
qCDebug(dcTuya()) << device->name() << "Controlling Tuya switch. Parent:" << parentDevice->name() << "command:" << command << "value:" << value;
|
||||
|
||||
pluginStorage()->beginGroup(parentDevice->id().toString());
|
||||
QString accesToken = pluginStorage()->value("accessToken").toString();
|
||||
pluginStorage()->endGroup();
|
||||
|
||||
QUrl url("http://px1.tuyaeu.com/homeassistant/skill");
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QVariantMap header;
|
||||
header.insert("name", command);
|
||||
header.insert("namespace", "control");
|
||||
header.insert("payloadVersion", 1);
|
||||
|
||||
QVariantMap payload;
|
||||
payload.insert("accessToken", accesToken);
|
||||
payload.insert("devId", device->paramValue(tuyaSwitchDeviceIdParamTypeId).toString());
|
||||
payload.insert("value", value);
|
||||
|
||||
QVariantMap data;
|
||||
data.insert("header", header);
|
||||
data.insert("payload", payload);
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromVariant(data);
|
||||
|
||||
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, jsonDoc.toJson(QJsonDocument::Compact));
|
||||
connect(reply, &QNetworkReply::finished, [reply](){reply->deleteLater();});
|
||||
connect(reply, &QNetworkReply::finished, info, [info, reply](){
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qCWarning(dcTuya()) << "Error setting switch state" << reply->error();
|
||||
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Error connecting to Tuya switch."));
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcTuya()) << "Json parser error in control switch reply" << error.errorString() << data;
|
||||
info->finish(Device::DeviceErrorHardwareFailure, QT_TR_NOOP("Received an unexpected reply from the Tuya switch."));
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap dataMap = jsonDoc.toVariant().toMap();
|
||||
bool success = dataMap.value("header").toMap().value("code").toString() == "SUCCESS";
|
||||
if (!success) {
|
||||
qCWarning(dcTuya()) << "Tuya response indicates an issue...";
|
||||
info->finish(Device::DeviceErrorHardwareFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(dcTuya()) << "Device controlled";
|
||||
info->finish(Device::DeviceErrorNoError);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* *
|
||||
* Copyright (C) 2019 Michael Zanetti <michael.zanetti@nymea.io> *
|
||||
* *
|
||||
* This file is part of nymea. *
|
||||
* *
|
||||
* nymea is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, version 2 of the License. *
|
||||
* *
|
||||
* nymea is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with nymea. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef DEVICEPLUGINTUYA_H
|
||||
#define DEVICEPLUGINTUYA_H
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#include "devices/deviceplugin.h"
|
||||
|
||||
class PluginTimer;
|
||||
|
||||
class DevicePluginTuya: public DevicePlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "deviceplugintuya.json")
|
||||
Q_INTERFACES(DevicePlugin)
|
||||
|
||||
|
||||
public:
|
||||
explicit DevicePluginTuya(QObject *parent = nullptr);
|
||||
~DevicePluginTuya() override;
|
||||
|
||||
void setupDevice(DeviceSetupInfo *info) override;
|
||||
void postSetupDevice(Device *device) override;
|
||||
void deviceRemoved(Device *device) override;
|
||||
void startPairing(DevicePairingInfo *info) override;
|
||||
void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override;
|
||||
void executeAction(DeviceActionInfo *info) override;
|
||||
|
||||
signals:
|
||||
void tokenRefreshed(Device *device, bool success);
|
||||
|
||||
private:
|
||||
void refreshAccessToken(Device *device);
|
||||
void updateChildDevices(Device *device);
|
||||
|
||||
void controlTuyaSwitch(const QString &command, const QString &value, DeviceActionInfo *info);
|
||||
|
||||
QHash<DeviceId, QTimer*> m_tokenExpiryTimers;
|
||||
PluginTimer *m_pluginTimer = nullptr;
|
||||
};
|
||||
|
||||
#endif // DEVICEPLUGINTUYA_H
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"name": "tuya",
|
||||
"displayName": "Tuya",
|
||||
"id": "405643b3-22ec-4a36-9808-e8b1405b01c9",
|
||||
"vendors": [
|
||||
{
|
||||
"name": "tuya",
|
||||
"displayName": "Tuya",
|
||||
"id": "d5dd33a7-e5f6-48be-bdd9-1a1ec04152c9",
|
||||
"deviceClasses": [
|
||||
{
|
||||
"id": "dd6dcd91-f667-45a5-9594-12b95f94337e",
|
||||
"name": "tuyaCloud",
|
||||
"displayName": "Tuya cloud login",
|
||||
"createMethods": ["user"],
|
||||
"setupMethod": "userandpassword",
|
||||
"interfaces": [ "account" ],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "c844a23a-301b-4e6c-ba18-2926a38e6bf5",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"displayNameEvent": "Connected changed",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "33e9d30c-c988-4cd4-8d39-ce84bcfa79c5",
|
||||
"name": "loggedIn",
|
||||
"displayName": "Logged in",
|
||||
"displayNameEvent": "Logged in changed",
|
||||
"type": "bool",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"id": "15bc38a7-0962-4f3b-a0b1-4f164958493b",
|
||||
"name": "userDisplayName",
|
||||
"displayName": "Username",
|
||||
"displayNameEvent": "User changed",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "393d7256-e792-4dca-adb5-b13750e05391",
|
||||
"name": "tuyaSwitch",
|
||||
"displayName": "Tuya switch",
|
||||
"createMethods": ["auto"],
|
||||
"interfaces": ["powersocket", "connectable"],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "bfdb02b0-d12d-4385-a03d-d2c147c2aca2",
|
||||
"name": "id",
|
||||
"displayName": "ID",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "b5ac83c4-e1ff-4682-80f2-61cca097ed8f",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"displayNameEvent": "Connected changed",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "c84a703a-d1c7-491d-9507-5a69b217ac53",
|
||||
"name": "power",
|
||||
"displayName": "Power",
|
||||
"displayNameEvent": "Power changed",
|
||||
"displayNameAction": "Set power",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"writable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d4bb0170-596d-4904-8fd0-fd8e7ad39f72",
|
||||
"name": "tuyaClosable",
|
||||
"displayName": "Tuya blinds",
|
||||
"createMethods": ["auto"],
|
||||
"interfaces": ["blind", "connectable"],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "b9b2bb1f-b44b-43d7-8bbb-e67cf1b5d0a0",
|
||||
"name": "id",
|
||||
"displayName": "ID",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "cf051676-3041-4e90-8c37-63e98412dfe8",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"displayNameEvent": "Connected changed",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
}
|
||||
],
|
||||
"actionTypes": [
|
||||
{
|
||||
"id": "f9f34515-670d-439e-851a-0bf82f867882",
|
||||
"name": "open",
|
||||
"displayName": "Open blinds"
|
||||
},
|
||||
{
|
||||
"id": "f266ea4a-052f-466b-bf31-8c5c7e80a843",
|
||||
"name": "close",
|
||||
"displayName": "Close blinds"
|
||||
},
|
||||
{
|
||||
"id": "8fc2ea7d-b945-41c8-bbda-820bdadc9e02",
|
||||
"name": "stop",
|
||||
"displayName": "Stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
include(../plugins.pri)
|
||||
|
||||
QT += network
|
||||
|
||||
PKGCONFIG += nymea-mqtt
|
||||
|
||||
TARGET = $$qtLibraryTarget(nymea_deviceplugintuya)
|
||||
|
||||
SOURCES += \
|
||||
deviceplugintuya.cpp \
|
||||
|
||||
HEADERS += \
|
||||
deviceplugintuya.h \
|
||||
Loading…
Reference in New Issue