New Plugin: tp-link Kasa smart plugs
parent
9fca1ed4e1
commit
e9ea2d919e
|
|
@ -564,6 +564,21 @@ 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-udpcommander
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends},
|
||||
|
|
@ -852,6 +867,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
|
||||
|
|
@ -46,6 +46,7 @@ PLUGIN_DIRS = \
|
|||
tasmota \
|
||||
tcpcommander \
|
||||
texasinstruments \
|
||||
tplink \
|
||||
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>
|
||||
Loading…
Reference in New Issue