From 7ee77fbc92b811c849cf5ae011499a2d4877b18d Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 27 Nov 2018 23:30:09 +0100 Subject: [PATCH] add a generic MQTT client plugin --- mqtt/devicepluginmqtt.cpp | 195 +++++++++++++++++ mqtt/devicepluginmqtt.h | 61 ++++++ mqtt/devicepluginmqtt.json | 196 ++++++++++++++++++ mqtt/mqtt.pro | 13 ++ ...58205-07c8-4482-85ad-b435387803a5-en_US.ts | 4 + nymea-plugins.pro | 1 + 6 files changed, 470 insertions(+) create mode 100644 mqtt/devicepluginmqtt.cpp create mode 100644 mqtt/devicepluginmqtt.h create mode 100644 mqtt/devicepluginmqtt.json create mode 100644 mqtt/mqtt.pro create mode 100644 mqtt/translations/27c58205-07c8-4482-85ad-b435387803a5-en_US.ts diff --git a/mqtt/devicepluginmqtt.cpp b/mqtt/devicepluginmqtt.cpp new file mode 100644 index 00000000..34359967 --- /dev/null +++ b/mqtt/devicepluginmqtt.cpp @@ -0,0 +1,195 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Michael Zanetti * + * * + * 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 * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/*! + \page mqtt.html + \title Generic MQTT + \brief Plugin for catching UDP commands from the network. + + \ingroup plugins + \ingroup nymea-plugins-maker + + This plugin allows to receive UDP packages over a certain UDP port and generates an \l{Event} if the message content matches + the \l{Param} command. + + \note This plugin is ment to be combined with a \l{nymeaserver::Rule}. + + \section3 Example + + If you create an UDP Commander on port 2323 and with the command \c{"Light 1 ON"}, following command will trigger an \l{Event} in nymea + and allows you to connect this \l{Event} with a \l{nymeaserver::Rule}. + + \note In this example nymea is running on \c localhost + + \code + $ echo "Light 1 ON" | nc -u localhost 2323 + OK + \endcode + + This allows you to execute \l{Action}{Actions} in your home automation system when a certain UDP message will be sent to nymea. + + If the command will be recognized from nymea, the sender will receive as answere a \c{"OK"} string. + + \chapter Plugin properties + Following JSON file contains the definition and the description of all available \l{DeviceClass}{DeviceClasses} + and \l{Vendor}{Vendors} of this \l{DevicePlugin}. + + For more details how to read this JSON file please check out the documentation for \l{The plugin JSON File}. + + \quotefile plugins/deviceplugins/udpcommander/devicepluginudpcommander.json +*/ + +#include "devicepluginmqtt.h" +#include "plugin/device.h" +#include "plugininfo.h" +#include "network/mqtt/mqttprovider.h" + +#include "nymea-mqtt/mqttclient.h" + +DevicePluginMqtt::DevicePluginMqtt() +{ + +} + +DeviceManager::DeviceSetupStatus DevicePluginMqtt::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::subscribed, 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, &DevicePluginMqtt::publishReceived); + connect(client, &MqttClient::published, this, &DevicePluginMqtt::published); + // In case we're already connected, manually call subscribe now + if (client->isConnected()) { + subscribe(device); + } + + return DeviceManager::DeviceSetupStatusAsync; +} + + +DeviceManager::DeviceError DevicePluginMqtt::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 DevicePluginMqtt::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 DevicePluginMqtt::publishReceived(const QString &topic, const QByteArray &payload, bool retained) +{ + qCDebug(dcMqttclient()) << "Publish received" << topic << payload << retained; + + MqttClient* client = static_cast(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 DevicePluginMqtt::published(quint16 packetId) +{ + if (!m_pendingPublishes.contains(packetId)) { + return; + } + + emit actionExecutionFinished(m_pendingPublishes.take(packetId).id(), DeviceManager::DeviceErrorNoError); +} + +void DevicePluginMqtt::deviceRemoved(Device *device) +{ + qCDebug(dcMqttclient) << device; + m_clients.take(device)->deleteLater(); +} + diff --git a/mqtt/devicepluginmqtt.h b/mqtt/devicepluginmqtt.h new file mode 100644 index 00000000..0289e4a1 --- /dev/null +++ b/mqtt/devicepluginmqtt.h @@ -0,0 +1,61 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Michael Zanetti * + * * + * 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 * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINMQTT_H +#define DEVICEPLUGINMQTT_H + +#include "plugin/deviceplugin.h" + +#include +#include +#include + +class MqttClient; + +class DevicePluginMqtt: public DevicePlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginmqtt.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginMqtt(); + + 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 m_clients; + + QHash m_pendingPublishes; +}; + +#endif // DEVICEPLUGINMQTT_H diff --git a/mqtt/devicepluginmqtt.json b/mqtt/devicepluginmqtt.json new file mode 100644 index 00000000..5a19379d --- /dev/null +++ b/mqtt/devicepluginmqtt.json @@ -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 + } + ] + } + ] + } + ] + } + ] +} diff --git a/mqtt/mqtt.pro b/mqtt/mqtt.pro new file mode 100644 index 00000000..6a97ecc0 --- /dev/null +++ b/mqtt/mqtt.pro @@ -0,0 +1,13 @@ +include(../plugins.pri) + +QT += network + +TARGET = $$qtLibraryTarget(nymea_devicepluginmqtt) + +SOURCES += \ + devicepluginmqtt.cpp + +HEADERS += \ + devicepluginmqtt.h + + diff --git a/mqtt/translations/27c58205-07c8-4482-85ad-b435387803a5-en_US.ts b/mqtt/translations/27c58205-07c8-4482-85ad-b435387803a5-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/mqtt/translations/27c58205-07c8-4482-85ad-b435387803a5-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 286032e9..fe31342b 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -21,6 +21,7 @@ PLUGIN_DIRS = \ leynew \ lgsmarttv \ mailnotification \ + mqtt \ netatmo \ networkdetector \ openweathermap \