Merge PR #164: New Plugin: Tuya cloud

master
Jenkins nymea 2020-01-30 17:24:44 +01:00
commit 359c6c98ed
16 changed files with 1543 additions and 0 deletions

31
debian/control vendored
View File

@ -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,

1
debian/nymea-plugin-tplink.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugintplink.so

1
debian/nymea-plugin-tuya.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugintuya.so

View File

@ -46,6 +46,8 @@ PLUGIN_DIRS = \
tasmota \
tcpcommander \
texasinstruments \
tplink \
tuya \
udpcommander \
unitec \
wakeonlan \

9
tplink/README.md Normal file
View File

@ -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.

View File

@ -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;
}
}

View File

@ -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

View File

@ -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
}
]
}
]
}
]
}

9
tplink/tplink.pro Normal file
View File

@ -0,0 +1,9 @@
include(../plugins.pri)
QT += network
SOURCES += \
deviceplugintplink.cpp \
HEADERS += \
deviceplugintplink.h \

View File

@ -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>

View File

@ -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>

5
tuya/README.md Normal file
View File

@ -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.

457
tuya/deviceplugintuya.cpp Normal file
View File

@ -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);
});
}

62
tuya/deviceplugintuya.h Normal file
View File

@ -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

130
tuya/deviceplugintuya.json Normal file
View File

@ -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"
}
]
}
]
}
]
}

13
tuya/tuya.pro Normal file
View File

@ -0,0 +1,13 @@
include(../plugins.pri)
QT += network
PKGCONFIG += nymea-mqtt
TARGET = $$qtLibraryTarget(nymea_deviceplugintuya)
SOURCES += \
deviceplugintuya.cpp \
HEADERS += \
deviceplugintuya.h \