mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-10-15 13:23:27 +02:00
Merge PR #63: New plugin: Generic MQTT client plugin (mqttclient)
This commit is contained in:
commit
719b861f94
5
debian/changelog
vendored
5
debian/changelog
vendored
@ -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
33
debian/control
vendored
@ -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
|
||||||
|
|||||||
1
debian/nymea-plugin-mqttclient.install.in
vendored
Normal file
1
debian/nymea-plugin-mqttclient.install.in
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginmqttclient.so
|
||||||
1
debian/nymea-plugin-tasmota.install.in
vendored
Normal file
1
debian/nymea-plugin-tasmota.install.in
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugintasmota.so
|
||||||
188
mqttclient/devicepluginmqttclient.cpp
Normal file
188
mqttclient/devicepluginmqttclient.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
61
mqttclient/devicepluginmqttclient.h
Normal file
61
mqttclient/devicepluginmqttclient.h
Normal 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
|
||||||
196
mqttclient/devicepluginmqttclient.json
Normal file
196
mqttclient/devicepluginmqttclient.json
Normal 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
13
mqttclient/mqttclient.pro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
include(../plugins.pri)
|
||||||
|
|
||||||
|
QT += network
|
||||||
|
|
||||||
|
TARGET = $$qtLibraryTarget(nymea_devicepluginmqttclient)
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
devicepluginmqttclient.cpp
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
devicepluginmqttclient.h
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1">
|
||||||
|
</TS>
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
289
tasmota/deviceplugintasmota.cpp
Normal file
289
tasmota/deviceplugintasmota.cpp
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
66
tasmota/deviceplugintasmota.h
Normal file
66
tasmota/deviceplugintasmota.h
Normal 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
|
||||||
220
tasmota/deviceplugintasmota.json
Normal file
220
tasmota/deviceplugintasmota.json
Normal 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
11
tasmota/tasmota.pro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
include(../plugins.pri)
|
||||||
|
|
||||||
|
QT += network
|
||||||
|
|
||||||
|
TARGET = $$qtLibraryTarget(nymea_deviceplugintasmota)
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
deviceplugintasmota.cpp \
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
deviceplugintasmota.h \
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1">
|
||||||
|
</TS>
|
||||||
Loading…
x
Reference in New Issue
Block a user