Merge PR #63: New plugin: Generic MQTT client plugin (mqttclient)

This commit is contained in:
Jenkins 2018-12-12 14:44:54 +01:00
commit 719b861f94
15 changed files with 1131 additions and 37 deletions

5
debian/changelog vendored
View File

@ -1,3 +1,8 @@
nymea-plugins (0.10.0) UNRELEASED; urgency=medium
-- Michael Zanetti <michael.zanetti@guh.io> Thu, 29 Nov 2018 12:48:11 +0100
nymea-plugins (0.9.39) xenial; urgency=medium nymea-plugins (0.9.39) xenial; urgency=medium
[ Jenkins ] [ Jenkins ]

33
debian/control vendored
View File

@ -4,6 +4,7 @@ Priority: options
Maintainer: Michael Zanetti <michael.zanetti@guh.io> Maintainer: Michael Zanetti <michael.zanetti@guh.io>
Build-depends: debhelper (>= 0.0.0), Build-depends: debhelper (>= 0.0.0),
libnymea1-dev, libnymea1-dev,
libnymea-mqtt-dev,
libqt5serialport5-dev, libqt5serialport5-dev,
python:any, python:any,
qtbase5-dev, qtbase5-dev,
@ -284,6 +285,21 @@ Description: nymea.io plugin for mailnotification
This package will install the nymea.io plugin for mailnotification This package will install the nymea.io plugin for mailnotification
Package: nymea-plugin-mqttclient
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Description: nymea.io plugin for a generic MQTT client
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 a generic MQTT client plugin for nymea.io
Package: nymea-plugin-multisensor Package: nymea-plugin-multisensor
Architecture: any Architecture: any
Depends: ${shlibs:Depends}, Depends: ${shlibs:Depends},
@ -429,6 +445,21 @@ Description: nymea.io plugin for pushbullet
This package will install the nymea.io plugin for pushbullet This package will install the nymea.io plugin for pushbullet
Package: nymea-plugin-tasmota
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Description: nymea.io plugin for Sonoff-Tasmota 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 Tasmota-Sonoff devices
Package: nymea-plugin-udpcommander Package: nymea-plugin-udpcommander
Architecture: any Architecture: any
Depends: ${shlibs:Depends}, Depends: ${shlibs:Depends},
@ -684,6 +715,7 @@ Depends: nymea-plugin-awattar,
nymea-plugin-pushbullet, nymea-plugin-pushbullet,
nymea-plugin-usbwde, nymea-plugin-usbwde,
nymea-plugin-wakeonlan, nymea-plugin-wakeonlan,
nymea-plugin-tasmota,
nymea-plugin-wemo, nymea-plugin-wemo,
nymea-plugin-elgato, nymea-plugin-elgato,
nymea-plugin-senic, nymea-plugin-senic,
@ -709,6 +741,7 @@ Depends: nymea-plugin-commandlauncher,
nymea-plugin-genericelements, nymea-plugin-genericelements,
nymea-plugin-avahimonitor, nymea-plugin-avahimonitor,
nymea-plugin-gpio, nymea-plugin-gpio,
nymea-plugin-mqttclient,
nymea-plugin-remotessh, nymea-plugin-remotessh,
Replaces: guh-plugins-maker Replaces: guh-plugins-maker
Description: Plugins for nymea IoT server - Meta package for makers, tinkers and hackers Description: Plugins for nymea IoT server - Meta package for makers, tinkers and hackers

View File

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

View File

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

View File

@ -0,0 +1,188 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*!
\page mqtt.html
\title Generic MQTT
\brief Plugin for subscribing and publishing to an MQTT broker.
\ingroup plugins
\ingroup nymea-plugins-maker
This plugin allows to subscribe and publish to MQTT brokers (the nymea internal broker and external ones).
\note This plugin is ment to be combined with a \l{nymeaserver::Rule}.
\section3 Example
A device is configured to publish its state to a MQTT broker. Using this plugin the user can subscribe to
the same topic on that broker and monitor the device's state.
Publishing is also supported. This allows use cases such as controlling IoT things via MQTT by publishing
to topics such devices are subscribed to. Other possibilities are to use nymea as a "translator" between other
transport layers to MQTT. For instance a sensor might deliver sensor data via Bluetooth to Nymea and using this
MQTT plugin and a \l{nymeaserver::Rule} nymea can be configured to forward all those sensor values to a MQTT broker.
\chapter Plugin properties
The plugin implements the inputtrigger and outputtrigger interfaces, adding parameters for topic and payload to
the trigger action and triggered event.
For more details how to read this JSON file please check out the documentation for \l{The plugin JSON File}.
\quotefile plugins/deviceplugins/mqtt/devicepluginmqttclient.json
*/
#include "devicepluginmqttclient.h"
#include "plugin/device.h"
#include "plugininfo.h"
#include "network/mqtt/mqttprovider.h"
#include "nymea-mqtt/mqttclient.h"
DevicePluginMqttClient::DevicePluginMqttClient()
{
}
DeviceManager::DeviceSetupStatus DevicePluginMqttClient::setupDevice(Device *device)
{
MqttClient *client = nullptr;
if (device->deviceClassId() == internalMqttClientDeviceClassId) {
client = hardwareManager()->mqttProvider()->createInternalClient(device->id());
} else if (device->deviceClassId() == mqttClientDeviceClassId){
client = new MqttClient("nymea-" + device->id().toString().remove(QRegExp("[{}]")).left(8), this);
client->setUsername(device->paramValue(mqttClientDeviceUsernameParamTypeId).toString());
client->setPassword(device->paramValue(mqttClientDevicePasswordParamTypeId).toString());
client->connectToHost(device->paramValue(mqttClientDeviceServerAddressParamTypeId).toString(), device->paramValue(mqttClientDeviceServerPortParamTypeId).toInt());
}
m_clients.insert(device, client);
connect(client, &MqttClient::connected, this, [this, device](){
subscribe(device);
});
connect(client, &MqttClient::subscribeResult, this, [this, device](quint16 packetId, const Mqtt::SubscribeReturnCodes returnCodes){
Q_UNUSED(packetId)
emit deviceSetupFinished(device, returnCodes.first() == Mqtt::SubscribeReturnCodeFailure ? DeviceManager::DeviceSetupStatusFailure : DeviceManager::DeviceSetupStatusSuccess);
});
connect(client, &MqttClient::publishReceived, this, &DevicePluginMqttClient::publishReceived);
connect(client, &MqttClient::published, this, &DevicePluginMqttClient::published);
// In case we're already connected, manually call subscribe now
if (client->isConnected()) {
subscribe(device);
}
return DeviceManager::DeviceSetupStatusAsync;
}
DeviceManager::DeviceError DevicePluginMqttClient::executeAction(Device *device, const Action &action)
{
ParamTypeId topicParamTypeId = internalMqttClientTriggerActionTopicParamTypeId;
ParamTypeId payloadParamTypeId = internalMqttClientTriggerActionDataParamTypeId;
ParamTypeId qosParamTypeId = internalMqttClientTriggerActionQosParamTypeId;
ParamTypeId retainParamTypeId = internalMqttClientTriggerActionRetainParamTypeId;
if (device->deviceClassId() == mqttClientDeviceClassId) {
topicParamTypeId = mqttClientTriggerActionTopicParamTypeId;
payloadParamTypeId = mqttClientTriggerActionDataParamTypeId;
qosParamTypeId = mqttClientTriggerActionQosParamTypeId;
retainParamTypeId = mqttClientTriggerActionRetainParamTypeId;
}
MqttClient *client = m_clients.value(device);
if (!client) {
qCWarning(dcMqttclient) << "No valid MQTT client for device" << device->name();
return DeviceManager::DeviceErrorDeviceNotFound;
}
Mqtt::QoS qos = Mqtt::QoS0;
switch (action.param(qosParamTypeId).value().toInt()) {
case 0:
qos = Mqtt::QoS0;
break;
case 1:
qos = Mqtt::QoS1;
break;
case 2:
qos = Mqtt::QoS2;
break;
}
quint16 packetId = client->publish(action.param(topicParamTypeId).value().toString(),
action.param(payloadParamTypeId).value().toByteArray(),
qos,
action.param(retainParamTypeId).value().toBool());
m_pendingPublishes.insert(packetId, action);
return DeviceManager::DeviceErrorAsync;
}
void DevicePluginMqttClient::subscribe(Device *device)
{
MqttClient *client = m_clients.value(device);
if (!client) {
// Device might have been removed
return;
}
if (device->deviceClassId() == internalMqttClientDeviceClassId) {
client->subscribe(device->paramValue(internalMqttClientDeviceTopicFilterParamTypeId).toString());
} else {
client->subscribe(device->paramValue(mqttClientDeviceTopicFilterParamTypeId).toString());
}
}
void DevicePluginMqttClient::publishReceived(const QString &topic, const QByteArray &payload, bool retained)
{
qCDebug(dcMqttclient()) << "Publish received" << topic << payload << retained;
MqttClient* client = static_cast<MqttClient*>(sender());
Device *device = m_clients.key(client);
if (!device) {
qCWarning(dcMqttclient) << "Received a publish message from a client where de don't have a matching device";
return;
}
EventTypeId eventTypeId = internalMqttClientTriggeredEventTypeId;
ParamTypeId topicParamTypeId = internalMqttClientTriggeredEventTopicParamTypeId;
ParamTypeId payloadParamTypeId = internalMqttClientTriggeredEventDataParamTypeId;
if (device->deviceClassId() == mqttClientDeviceClassId) {
eventTypeId = mqttClientTriggeredEventTypeId;
topicParamTypeId = mqttClientTriggeredEventTopicParamTypeId;
payloadParamTypeId = mqttClientTriggeredEventDataParamTypeId;
}
emitEvent(Event(eventTypeId, device->id(), ParamList() << Param(topicParamTypeId, topic) << Param(payloadParamTypeId, payload)));
}
void DevicePluginMqttClient::published(quint16 packetId)
{
if (!m_pendingPublishes.contains(packetId)) {
return;
}
emit actionExecutionFinished(m_pendingPublishes.take(packetId).id(), DeviceManager::DeviceErrorNoError);
}
void DevicePluginMqttClient::deviceRemoved(Device *device)
{
qCDebug(dcMqttclient) << device;
m_clients.take(device)->deleteLater();
}

View File

@ -0,0 +1,61 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef DEVICEPLUGINMQTTCLIENT_H
#define DEVICEPLUGINMQTTCLIENT_H
#include "plugin/deviceplugin.h"
#include <QHash>
#include <QDebug>
#include <QUdpSocket>
class MqttClient;
class DevicePluginMqttClient: public DevicePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginmqttclient.json")
Q_INTERFACES(DevicePlugin)
public:
explicit DevicePluginMqttClient();
DeviceManager::DeviceSetupStatus setupDevice(Device *device) override;
void deviceRemoved(Device *device) override;
DeviceManager::DeviceError executeAction(Device *device, const Action &action) override;
private slots:
void subscribe(Device *device);
void publishReceived(const QString &topic, const QByteArray &payload, bool retained);
void published(quint16 packetId);
private:
QHash<Device*, MqttClient*> m_clients;
QHash<quint16, Action> m_pendingPublishes;
};
#endif // DEVICEPLUGINMQTTCLIENT_H

View File

@ -0,0 +1,196 @@
{
"name": "mqttclient",
"displayName": "MQTT client",
"id": "27c58205-07c8-4482-85ad-b435387803a5",
"vendors": [
{
"name": "guh",
"displayName": "guh GmbH",
"id": "2062d64d-3232-433c-88bc-0d33c0ba2ba6",
"deviceClasses": [
{
"id": "19117099-a5ef-44a1-b2bb-2efafe00f197",
"name": "internalMqttClient",
"displayName": "Internal MQTT client",
"interfaces": ["inputtrigger", "outputtrigger"],
"createMethods": ["user"],
"paramTypes": [
{
"id": "4e91772a-82d8-498f-8b62-bba90a682e76",
"name": "topicFilter",
"displayName": "Subscription topic filter",
"type": "QString",
"defaultValue": "#"
}
],
"eventTypes": [
{
"id": "d4ea2a70-da5a-49e0-9f30-aac1334b6a02",
"name": "triggered",
"displayName": "Publish received",
"paramTypes": [
{
"id": "27ec8baf-0c13-4d0a-aaee-313582592695",
"name": "topic",
"displayName": "Topic",
"type": "QString"
},
{
"id": "8af98566-79d9-4e65-b1dc-9067e4f93af1",
"name": "data",
"displayName": "Playload",
"type": "QString"
}
]
}
],
"actionTypes": [
{
"id": "2f90ff12-dd67-4ddf-815d-330b4e2d56bf",
"name": "trigger",
"displayName": "Publish",
"paramTypes": [
{
"id": "bed321c2-a8c4-4420-b831-c4faa8501115",
"name": "topic",
"displayName": "Topic",
"type": "QString",
"defaultValue": "/"
},
{
"id": "5bff6492-e6c7-4e50-a1c1-69881250561d",
"name": "data",
"displayName": "Payload",
"type": "QString",
"defaultValue": ""
},
{
"id": "b019b678-aaf1-46d0-a0f8-af2131f14e55",
"name": "qos",
"displayName": "QoS",
"type": "int",
"minValue": 0,
"maxValue": 2,
"defaultValue": 0
},
{
"id": "c2c6386e-5b7d-4a2a-a8e8-e9c259ba926b",
"name": "retain",
"displayName": "Retain message",
"type": "bool",
"defaultValue": false
}
]
}
]
},
{
"id": "e325b581-8d7f-446e-b761-67554c5aacd4",
"name": "mqttClient",
"displayName": "MQTT client",
"interfaces": ["inputtrigger", "outputtrigger"],
"createMethods": ["user"],
"paramTypes": [
{
"id": "a9a97dd6-9f80-43eb-a956-f5f3e4c6e3e2",
"name": "serverAddress",
"displayName": "Address",
"type": "QString",
"defaultValue": ""
},
{
"id": "91973ede-b64e-4cae-ae67-6087df79eeb4",
"name": "serverPort",
"displayName": "Port",
"type": "int",
"minValue": 0,
"maxValue": 65535,
"defaultValue": 1883
},
{
"id": "ae19fcc2-80ae-4d3f-8bac-4cf0db98d9e7",
"name": "username",
"displayName": "Username",
"type": "QString",
"defaultValue": ""
},
{
"id": "d8211599-52f7-46f6-a741-a7204b987309",
"name": "password",
"displayName": "Password",
"type": "QString",
"defaultValue": ""
},
{
"id": "53e2715a-e72f-445a-ae6b-2ac4e6031114",
"name": "topicFilter",
"displayName": "Subscription topic filter",
"type": "QString",
"defaultValue": "#"
}
],
"eventTypes": [
{
"id": "243ec6ee-a72e-47e0-91dd-b9b918c43072",
"name": "triggered",
"displayName": "Publish received",
"paramTypes": [
{
"id": "bd83c7ec-3a14-46c6-a064-25757ceb0207",
"name": "topic",
"displayName": "Topic",
"type": "QString"
},
{
"id": "a947a277-a17a-4cb2-addb-f8ecec1cc63c",
"name": "data",
"displayName": "Playload",
"type": "QString"
}
]
}
],
"actionTypes": [
{
"id": "39df4723-c888-4a3f-a151-9408699a9d25",
"name": "trigger",
"displayName": "Publish",
"paramTypes": [
{
"id": "193655ec-1714-4ea0-b8ee-f1dc312f15d3",
"name": "topic",
"displayName": "Topic",
"type": "QString",
"defaultValue": "/"
},
{
"id": "a0e8989b-2797-4447-8d67-408382bfebae",
"name": "data",
"displayName": "Payload",
"type": "QString",
"defaultValue": ""
},
{
"id": "4d2130be-8123-4103-b0bb-43ba876e147f",
"name": "qos",
"displayName": "QoS",
"type": "int",
"minValue": 0,
"maxValue": 2,
"defaultValue": 0
},
{
"id": "097774cc-7947-4eb1-bd30-ec4566afa628",
"name": "retain",
"displayName": "Retain message",
"type": "bool",
"defaultValue": false
}
]
}
]
}
]
}
]
}

13
mqttclient/mqttclient.pro Normal file
View File

@ -0,0 +1,13 @@
include(../plugins.pri)
QT += network
TARGET = $$qtLibraryTarget(nymea_devicepluginmqttclient)
SOURCES += \
devicepluginmqttclient.cpp
HEADERS += \
devicepluginmqttclient.h

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
</TS>

View File

@ -1,46 +1,48 @@
TEMPLATE = subdirs TEMPLATE = subdirs
PLUGIN_DIRS = \ PLUGIN_DIRS = \
elro \
intertechno \
networkdetector \
conrad \
openweathermap \
wakeonlan \
mailnotification \
philipshue \
eq-3 \
wemo \
lgsmarttv \
datetime \
genericelements \
commandlauncher \
unitec \
leynew \
udpcommander \
tcpcommander \
httpcommander \
kodi \
elgato \
senic \
awattar \
netatmo \
plantcare \
osdomotics \
ws2812 \
orderbutton \
denon \
avahimonitor \ avahimonitor \
pushbullet \ awattar \
usbwde \ commandlauncher \
multisensor \ conrad \
gpio \ datetime \
snapd \ denon \
simulation \
keba \
remotessh \
dweetio \ dweetio \
elgato \
elro \
eq-3 \
flowercare \ flowercare \
genericelements \
gpio \
httpcommander \
intertechno \
keba \
kodi \
leynew \
lgsmarttv \
mailnotification \
multisensor \
mqttclient \
netatmo \
networkdetector \
openweathermap \
orderbutton \
osdomotics \
philipshue \
plantcare \
pushbullet \
remotessh \
senic \
simulation \
snapd \
tasmota \
tcpcommander \
udpcommander \
unitec \
usbwde \
wakeonlan \
wemo \
ws2812 \
CONFIG+=all CONFIG+=all

View File

@ -0,0 +1,289 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 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/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*!
\page tasmota.html
\title Tasmota (Sonoff) devices
\brief Plugin for Tasmota devices (Sonoff devices flashed with Tasmota and others)
\ingroup plugins
\ingroup nymea-plugins-maker
This plugin allows to make use of Sonoff-Tasmota devices via the nymea internal MQTT broker.
There is no external MQTT broker needed.
Note that Sonoff devices must be flashed with the Tasmota sofware and connected to the WiFi network in order to
work with this plugin.
See https://github.com/arendst/Sonoff-Tasmota/wiki for a list of all supported devices and instructions on how to
install Tasmota on those.
After flashing Tasmota to a Sonoff device and connecting it to WiFi, it can be added to nymea. The only required
thing is the IP address to the device. This plugin will create a new isoloated MQTT channel on the nymea internal
MQTT broker and provision login details to the Tasmota device via HTTP. Once that is successful, the Tasmota device
will connect to the MQTT broker and appear as connected in nymea.
\chapter Plugin properties
When adding a Tasmota device it will add a new Gateway type device representing the Tasmota device itself. In addition
to that a power switch device will appear which can be used to control the switches in the Tasmota device. Upon
device setup, the user can optionally select the type of the connected hardware, (e.g. a light) which causes this
plugin to create a light device in the system which also controls the switches inside the Tasmota device and nicely
integrates with the nymea:ux for the given device type.
For more details how to read this JSON file please check out the documentation for \l{The plugin JSON File}.
\quotefile plugins/deviceplugins/tasmota/deviceplugintasmota.json
*/
#include "deviceplugintasmota.h"
#include "plugininfo.h"
#include <QUrlQuery>
#include <QNetworkReply>
#include <QHostAddress>
#include <QJsonDocument>
#include "hardwaremanager.h"
#include "network/networkaccessmanager.h"
#include "network/mqtt/mqttprovider.h"
#include "network/mqtt/mqttchannel.h"
DevicePluginTasmota::DevicePluginTasmota()
{
// Helper maps for parent devices (aka sonoff_*)
m_ipAddressParamTypeMap[sonoff_basicDeviceClassId] = sonoff_basicDeviceIpAddressParamTypeId;
m_ipAddressParamTypeMap[sonoff_dualDeviceClassId] = sonoff_dualDeviceIpAddressParamTypeId;
m_ipAddressParamTypeMap[sonoff_quadDeviceClassId] = sonoff_quadDeviceIpAddressParamTypeId;
m_attachedDeviceParamTypeIdMap[sonoff_basicDeviceClassId] << sonoff_basicDeviceAttachedDeviceCH1ParamTypeId;
m_attachedDeviceParamTypeIdMap[sonoff_dualDeviceClassId] << sonoff_dualDeviceAttachedDeviceCH1ParamTypeId << sonoff_dualDeviceAttachedDeviceCH2ParamTypeId;
m_attachedDeviceParamTypeIdMap[sonoff_quadDeviceClassId] << sonoff_quadDeviceAttachedDeviceCH1ParamTypeId << sonoff_quadDeviceAttachedDeviceCH2ParamTypeId << sonoff_quadDeviceAttachedDeviceCH3ParamTypeId << sonoff_quadDeviceAttachedDeviceCH4ParamTypeId;
// Helper maps for virtual childs (aka tasmota*)
m_channelParamTypeMap[tasmotaSwitchDeviceClassId] = tasmotaSwitchDeviceChannelNameParamTypeId;
m_channelParamTypeMap[tasmotaLightDeviceClassId] = tasmotaLightDeviceChannelNameParamTypeId;
m_powerStateTypeMap[tasmotaSwitchDeviceClassId] = tasmotaSwitchPowerStateTypeId;
m_powerStateTypeMap[tasmotaLightDeviceClassId] = tasmotaLightPowerStateTypeId;
// Helper maps for all devices
m_connectedStateTypeMap[sonoff_basicDeviceClassId] = sonoff_basicConnectedStateTypeId;
m_connectedStateTypeMap[sonoff_dualDeviceClassId] = sonoff_dualConnectedStateTypeId;
m_connectedStateTypeMap[sonoff_quadDeviceClassId] = sonoff_quadConnectedStateTypeId;
m_connectedStateTypeMap[tasmotaSwitchDeviceClassId] = tasmotaSwitchConnectedStateTypeId;
m_connectedStateTypeMap[tasmotaLightDeviceClassId] = tasmotaLightConnectedStateTypeId;
}
DevicePluginTasmota::~DevicePluginTasmota()
{
}
void DevicePluginTasmota::init()
{
}
DeviceManager::DeviceSetupStatus DevicePluginTasmota::setupDevice(Device *device)
{
if (m_ipAddressParamTypeMap.contains(device->deviceClassId())) {
ParamTypeId ipAddressParamTypeId = m_ipAddressParamTypeMap.value(device->deviceClassId());
QHostAddress deviceAddress = QHostAddress(device->paramValue(ipAddressParamTypeId).toString());
if (deviceAddress.isNull()) {
qCWarning(dcTasmota) << "Not a valid IP address given for IP address parameter";
return DeviceManager::DeviceSetupStatusFailure;
}
MqttChannel *channel = hardwareManager()->mqttProvider()->createChannel(device->id(), deviceAddress);
if (!channel) {
qCWarning(dcTasmota) << "Failed to create MQTT channel.";
return DeviceManager::DeviceSetupStatusFailure;
}
QUrl url("http://10.10.10.90/sv");
QUrlQuery query;
query.addQueryItem("w", "2%2C1");
query.addQueryItem("mh", channel->serverAddress().toString());
query.addQueryItem("ml", QString::number(channel->serverPort()));
query.addQueryItem("mc", channel->clientId());
query.addQueryItem("mu", channel->username());
query.addQueryItem("mp", channel->password());
query.addQueryItem("mt", "sonoff");
query.addQueryItem("mf", channel->topicPrefix() + "/%topic%/");
url.setQuery(query);
QNetworkRequest request(url);
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, this, [this, device, channel, reply](){
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qCDebug(dcTasmota) << "Sonoff device setup call failed:" << reply->error() << reply->errorString() << reply->readAll();
hardwareManager()->mqttProvider()->releaseChannel(channel);
emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure);
return;
}
m_mqttChannels.insert(device, channel);
connect(channel, &MqttChannel::clientConnected, this, &DevicePluginTasmota::onClientConnected);
connect(channel, &MqttChannel::clientDisconnected, this, &DevicePluginTasmota::onClientDisconnected);
connect(channel, &MqttChannel::publishReceived, this, &DevicePluginTasmota::onPublishReceived);
qCDebug(dcTasmota) << "Sonoff setup complete";
emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess);
foreach (Device *child, myDevices()) {
if (child->parentId() == device->id()) {
// Already have child devices... We're done here
return;
}
}
qCDebug(dcTasmota) << "Adding Tasmota Switch devices";
QList<DeviceDescriptor> deviceDescriptors;
for (int i = 0; i < m_attachedDeviceParamTypeIdMap.value(device->deviceClassId()).count(); i++) {
DeviceDescriptor descriptor(tasmotaSwitchDeviceClassId, device->name() + " CH" + QString::number(i+1), QString(), device->id());
if (m_attachedDeviceParamTypeIdMap.value(device->deviceClassId()).count() == 1) {
descriptor.setParams(ParamList() << Param(tasmotaSwitchDeviceChannelNameParamTypeId, "POWER"));
} else {
descriptor.setParams(ParamList() << Param(tasmotaSwitchDeviceChannelNameParamTypeId, "POWER" + QString::number(i+1)));
}
deviceDescriptors << descriptor;
}
emit autoDevicesAppeared(tasmotaSwitchDeviceClassId, deviceDescriptors);
qCDebug(dcTasmota) << "Adding Tasmota connected devices";
deviceDescriptors.clear();
for (int i = 0; i < m_attachedDeviceParamTypeIdMap.value(device->deviceClassId()).count(); i++) {
ParamTypeId attachedDeviceParamTypeId = m_attachedDeviceParamTypeIdMap.value(device->deviceClassId()).at(i);
if (device->paramValue(attachedDeviceParamTypeId).toString() == "Light") {
DeviceDescriptor descriptor(tasmotaLightDeviceClassId, device->name() + " CH" + QString::number(i+1), QString(), device->id());
descriptor.setParentDeviceId(device->id());
if (m_attachedDeviceParamTypeIdMap.value(device->deviceClassId()).count() == 1) {
descriptor.setParams(ParamList() << Param(tasmotaLightDeviceChannelNameParamTypeId, "POWER"));
} else {
descriptor.setParams(ParamList() << Param(tasmotaLightDeviceChannelNameParamTypeId, "POWER" + QString::number(i+1)));
}
deviceDescriptors << descriptor;
}
}
if (!deviceDescriptors.isEmpty()) {
emit autoDevicesAppeared(tasmotaLightDeviceClassId, deviceDescriptors);
}
});
return DeviceManager::DeviceSetupStatusAsync;
}
if (m_connectedStateTypeMap.contains(device->deviceClassId())) {
Device* parentDevice = myDevices().findById(device->parentId());
StateTypeId connectedStateTypeId = m_connectedStateTypeMap.value(device->deviceClassId());
device->setStateValue(m_connectedStateTypeMap.value(device->deviceClassId()), parentDevice->stateValue(connectedStateTypeId));
return DeviceManager::DeviceSetupStatusSuccess;
}
qCWarning(dcTasmota) << "Unhandled DeviceClass in setupDevice" << device->deviceClassId();
return DeviceManager::DeviceSetupStatusFailure;
}
void DevicePluginTasmota::deviceRemoved(Device *device)
{
qCDebug(dcTasmota) << "Device removed" << device->name();
if (m_mqttChannels.contains(device)) {
qCDebug(dcTasmota) << "Releasing MQTT channel";
MqttChannel* channel = m_mqttChannels.take(device);
hardwareManager()->mqttProvider()->releaseChannel(channel);
}
}
DeviceManager::DeviceError DevicePluginTasmota::executeAction(Device *device, const Action &action)
{
if (m_powerStateTypeMap.contains(device->deviceClassId())) {
Device *parentDev = myDevices().findById(device->parentId());
MqttChannel *channel = m_mqttChannels.value(parentDev);
ParamTypeId channelParamTypeId = m_channelParamTypeMap.value(device->deviceClassId());
ParamTypeId powerActionParamTypeId = ParamTypeId(m_powerStateTypeMap.value(device->deviceClassId()).toString());
qCDebug(dcTasmota) << "Publishing:" << channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(channelParamTypeId).toString() << (action.param(powerActionParamTypeId).value().toBool() ? "ON" : "OFF");
channel->publish(channel->topicPrefix() + "/sonoff/cmnd/" + device->paramValue(channelParamTypeId).toString().toLower(), action.param(powerActionParamTypeId).value().toBool() ? "ON" : "OFF");
device->setStateValue(m_powerStateTypeMap.value(device->deviceClassId()), action.param(powerActionParamTypeId).value().toBool());
return DeviceManager::DeviceErrorNoError;
}
return DeviceManager::DeviceErrorDeviceClassNotFound;
}
void DevicePluginTasmota::onClientConnected(MqttChannel *channel)
{
qCDebug(dcTasmota) << "Sonoff device connected!";
Device *dev = m_mqttChannels.key(channel);
dev->setStateValue(m_connectedStateTypeMap.value(dev->deviceClassId()), true);
foreach (Device *child, myDevices()) {
if (child->parentId() == dev->id()) {
child->setStateValue(m_connectedStateTypeMap.value(child->deviceClassId()), true);
}
}
}
void DevicePluginTasmota::onClientDisconnected(MqttChannel *channel)
{
qCDebug(dcTasmota) << "Sonoff device disconnected!";
Device *dev = m_mqttChannels.key(channel);
dev->setStateValue(m_connectedStateTypeMap.value(dev->deviceClassId()), false);
foreach (Device *child, myDevices()) {
if (child->parentId() == dev->id()) {
child->setStateValue(m_connectedStateTypeMap.value(child->deviceClassId()), false);
}
}
}
void DevicePluginTasmota::onPublishReceived(MqttChannel *channel, const QString &topic, const QByteArray &payload)
{
qCDebug(dcTasmota) << "Publish received from Sonoff device:" << topic << payload;
Device *dev = m_mqttChannels.key(channel);
if (m_ipAddressParamTypeMap.contains(dev->deviceClassId())) {
if (topic.startsWith(channel->topicPrefix() + "/sonoff/POWER")) {
QString channelName = topic.split("/").last();
foreach (Device *child, myDevices()) {
if (child->parentId() != dev->id()) {
continue;
}
if (child->paramValue(m_channelParamTypeMap.value(child->deviceClassId())).toString() != channelName) {
continue;
}
child->setStateValue(m_powerStateTypeMap.value(child->deviceClassId()), payload == "ON");
}
}
if (topic.startsWith(channel->topicPrefix() + "/sonoff/STATE")) {
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(payload, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcTasmota) << "Cannot parse JSON from Tasmota device" << error.errorString();
return;
}
foreach (Device *child, myDevices()) {
if (child->parentId() != dev->id()) {
continue;
}
QString childChannel = child->paramValue(m_channelParamTypeMap.value(child->deviceClassId())).toString();
QString valueString = jsonDoc.toVariant().toMap().value(childChannel).toString();
child->setStateValue(m_powerStateTypeMap.value(child->deviceClassId()), valueString == "ON");
}
}
}
}

View File

@ -0,0 +1,66 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 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 DEVICEPLUGINTASMOTA_H
#define DEVICEPLUGINTASMOTA_H
#include "plugin/deviceplugin.h"
#include "devicemanager.h"
class MqttChannel;
class DevicePluginTasmota: public DevicePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "deviceplugintasmota.json")
Q_INTERFACES(DevicePlugin)
public:
explicit DevicePluginTasmota();
~DevicePluginTasmota();
void init() override;
DeviceManager::DeviceSetupStatus setupDevice(Device *device) override;
void deviceRemoved(Device *device) override;
DeviceManager::DeviceError executeAction(Device *device, const Action &action) override;
private slots:
void onClientConnected(MqttChannel *channel);
void onClientDisconnected(MqttChannel *channel);
void onPublishReceived(MqttChannel *channel, const QString &topic, const QByteArray &payload);
private:
QHash<Device*, MqttChannel*> m_mqttChannels;
// Helpers for parent devices (the ones starting with sonoff)
QHash<DeviceClassId, ParamTypeId> m_ipAddressParamTypeMap;
QHash<DeviceClassId, QList<ParamTypeId> > m_attachedDeviceParamTypeIdMap;
// Helpers for child devices (virtual ones, starting with tasmota)
QHash<DeviceClassId, ParamTypeId> m_channelParamTypeMap;
QHash<DeviceClassId, StateTypeId> m_powerStateTypeMap;
// Helpers for both devices
QHash<DeviceClassId, StateTypeId> m_connectedStateTypeMap;
};
#endif // DEVICEPLUGINTASMOTA_H

View File

@ -0,0 +1,220 @@
{
"name": "tasmota",
"displayName": "Sonoff-Tasmota",
"id": "d136e0c0-0cbf-4731-aabb-b2201088d6cb",
"vendors": [
{
"name": "tasmota",
"displayName": "Sonoff-Tasmota",
"id": "789e6173-3de3-44ef-8664-9492c9d15d44",
"deviceClasses": [
{
"id": "f39fdfa8-73e0-4cf4-8d05-dc237ced7a57",
"name": "sonoff_basic",
"displayName": "Single switch (Sonoff Basic, RF, Touch...)",
"createMethods": ["user"],
"interfaces": [ "gateway" ],
"paramTypes": [
{
"id": "cdead654-a765-488c-9fe6-ce6afb550d8b",
"name":"ipAddress",
"displayName": "IP address",
"type": "QString"
},
{
"id": "f210d0c0-dda1-442d-a0cc-2f2e48c24984",
"name": "attachedDeviceCH1",
"displayName": "Connected device",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
}
],
"stateTypes": [
{
"id": "9cde6321-2abf-4a58-a1d6-c7418edb9747",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
}
]
},
{
"id": "425ab191-833c-4618-8ac8-aff02370b99d",
"name": "sonoff_dual",
"displayName": "Dual switch (Sonoff Dual, T1 2CH...)",
"createMethods": ["user"],
"interfaces": [ "gateway" ],
"paramTypes": [
{
"id": "7fe081a4-b9ec-4ca5-b583-50e992a24f4d",
"name":"ipAddress",
"displayName": "IP address",
"type": "QString"
},
{
"id": "5101ad0d-c887-44b8-998d-021948184ccd",
"name": "attachedDeviceCH1",
"displayName": "Connected device 1",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
},
{
"id": "530edfb0-930d-4885-b1c0-3bf51a7671f1",
"name": "attachedDeviceCH2",
"displayName": "Connected device 2",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
}
],
"stateTypes": [
{
"id": "e2f55332-e706-412e-beb6-abf76b3bcff3",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
}
]
},
{
"id": "ae845ec9-be61-4bdf-9015-4c156f937da7",
"name": "sonoff_quad",
"displayName": "4 channel switch (Sonoff 4CH, T1 4CH...)",
"createMethods": ["user"],
"interfaces": [ "gateway" ],
"paramTypes": [
{
"id": "dbc3f3b3-2d17-40e9-8f6e-dde0b26952bc",
"name":"ipAddress",
"displayName": "IP address",
"type": "QString"
},
{
"id": "d6520a7a-d340-4f42-a8c4-b2da8434f40f",
"name": "attachedDeviceCH1",
"displayName": "Connected device 1",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
},
{
"id": "b27a1ad0-4455-4264-81a1-d625e312c330",
"name": "attachedDeviceCH2",
"displayName": "Connected device 2",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
},
{
"id": "ee2509fb-7690-47f7-af74-05635b460be7",
"name": "attachedDeviceCH3",
"displayName": "Connected device 3",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
},
{
"id": "a5bdb44f-9789-4e0a-8274-ec7866e8f148",
"name": "attachedDeviceCH4",
"displayName": "Connected device 4",
"type": "QString",
"allowedValues": ["None", "Light"],
"defaultValue": "None"
}
],
"stateTypes": [
{
"id": "5b422d28-9f60-4ea9-ab23-42a0ec605b9e",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
}
]
},
{
"id": "8a5e69c0-14ad-4ae8-9ff9-10055de6ffdf",
"name": "tasmotaSwitch",
"displayName": "Tasmota power switch",
"createMethods": ["auto"],
"interfaces": ["power", "connectable"],
"paramTypes": [
{
"id": "564cf6c6-86eb-41a5-9b87-fb32f1b6fcd6",
"name": "channelName",
"displayName": "Channel name",
"type": "QString"
}
],
"stateTypes": [
{
"id": "b4607e5d-70c4-4e76-9d9a-c6de7c50377e",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "413503d7-fc9f-417a-95fa-5c350a6f69f9",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Power changed",
"displayNameAction": "Set power",
"type": "bool",
"defaultValue": false,
"writable": true
}
]
},
{
"id": "83e5d9e6-5ac8-4e41-9717-481415048d49",
"name": "tasmotaLight",
"displayName": "Tasmota light",
"createMethods": ["auto"],
"interfaces": ["light", "connectable"],
"paramTypes": [
{
"id": "1f792ae4-cf39-4e12-99ca-c593bd020fcb",
"name": "channelName",
"displayName": "Channel name",
"type": "QString"
}
],
"stateTypes": [
{
"id": "72050de9-c318-4e53-93e5-36f7c2fc7cab",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "88dbdf8e-45ff-466f-8352-8654a6b5fe68",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Power changed",
"displayNameAction": "Set power",
"type": "bool",
"defaultValue": false,
"writable": true
}
]
}
]
}
]
}

11
tasmota/tasmota.pro Normal file
View File

@ -0,0 +1,11 @@
include(../plugins.pri)
QT += network
TARGET = $$qtLibraryTarget(nymea_deviceplugintasmota)
SOURCES += \
deviceplugintasmota.cpp \
HEADERS += \
deviceplugintasmota.h \

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
</TS>