diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 6c5b071f..286032e9 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -1,43 +1,44 @@ TEMPLATE = subdirs 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 \ - gpio \ - snapd \ - simulation \ - keba \ - remotessh \ + awattar \ + commandlauncher \ + conrad \ + datetime \ + denon \ dweetio \ + elgato \ + elro \ + eq-3 \ flowercare \ + genericelements \ + gpio \ + httpcommander \ + intertechno \ + keba \ + kodi \ + leynew \ + lgsmarttv \ + mailnotification \ + netatmo \ + networkdetector \ + openweathermap \ + orderbutton \ + osdomotics \ + philipshue \ + plantcare \ + remotessh \ + senic \ + simulation \ + snapd \ + tasmota \ + tcpcommander \ + udpcommander \ + unitec \ + wakeonlan \ + wemo \ + ws2812 \ CONFIG+=all diff --git a/tasmota/deviceplugintasmota.cpp b/tasmota/deviceplugintasmota.cpp new file mode 100644 index 00000000..c684e8aa --- /dev/null +++ b/tasmota/deviceplugintasmota.cpp @@ -0,0 +1,231 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Michael Zanetti * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "deviceplugintasmota.h" +#include "plugininfo.h" + +#include +#include +#include +#include + +#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); + + Q_UNUSED(device) + 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 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 descriptor1(tasmotaLightDeviceClassId, device->name() + " CH" + QString::number(i+1), QString(), device->id()); + if (m_attachedDeviceParamTypeIdMap.value(device->deviceClassId()).count() == 1) { + descriptor1.setParams(ParamList() << Param(tasmotaLightDeviceChannelNameParamTypeId, "POWER")); + } else { + descriptor1.setParams(ParamList() << Param(tasmotaLightDeviceChannelNameParamTypeId, "POWER" + QString::number(i+1))); + } + deviceDescriptors << descriptor1; + } + } + 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"); + } + return DeviceManager::DeviceErrorActionTypeNotFound; +} + +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(dev->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")) { + return; + } + 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"); + } + } +} + diff --git a/tasmota/deviceplugintasmota.h b/tasmota/deviceplugintasmota.h new file mode 100644 index 00000000..f73bde9b --- /dev/null +++ b/tasmota/deviceplugintasmota.h @@ -0,0 +1,66 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2018 Michael Zanetti * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#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 m_mqttChannels; + + // Helpers for parent devices (the ones starting with sonoff) + QHash m_ipAddressParamTypeMap; + QHash > m_attachedDeviceParamTypeIdMap; + + // Helpers for child devices (virtual ones, starting with tasmota) + QHash m_channelParamTypeMap; + QHash m_powerStateTypeMap; + + // Helpers for both devices + QHash m_connectedStateTypeMap; +}; + +#endif // DEVICEPLUGINTASMOTA_H diff --git a/tasmota/deviceplugintasmota.json b/tasmota/deviceplugintasmota.json new file mode 100644 index 00000000..7709ccb3 --- /dev/null +++ b/tasmota/deviceplugintasmota.json @@ -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": "Sonoff switch (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": "Sonoff dual switch (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", + "type": "QString", + "allowedValues": ["None", "Light"], + "defaultValue": "None" + }, + { + "id": "530edfb0-930d-4885-b1c0-3bf51a7671f1", + "name": "attachedDeviceCH2", + "displayName": "Connected device", + "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": "Sonoff 4 channel switch (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 + } + ] + } + ] + } + ] +} diff --git a/tasmota/tasmota.pro b/tasmota/tasmota.pro new file mode 100644 index 00000000..c70f70f7 --- /dev/null +++ b/tasmota/tasmota.pro @@ -0,0 +1,11 @@ +include(../plugins.pri) + +QT += network + +TARGET = $$qtLibraryTarget(nymea_deviceplugintasmota) + +SOURCES += \ + deviceplugintasmota.cpp \ + +HEADERS += \ + deviceplugintasmota.h \ diff --git a/tasmota/translations/d136e0c0-0cbf-4731-aabb-b2201088d6cb-en_US.ts b/tasmota/translations/d136e0c0-0cbf-4731-aabb-b2201088d6cb-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/tasmota/translations/d136e0c0-0cbf-4731-aabb-b2201088d6cb-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file