From 6640316cf31507f860fcba6d7d7673acd8c4b2f0 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 2 Jan 2019 22:49:47 +0100 Subject: [PATCH] New Plugin: ANEL Elektronik NET-PwrCtrl --- anel/anel.pro | 13 + anel/anelpanel.cpp | 6 + anel/anelpanel.h | 21 ++ anel/devicepluginanel.cpp | 307 ++++++++++++++++++ anel/devicepluginanel.h | 63 ++++ anel/devicepluginanel.json | 82 +++++ ...e5b64-20e4-42bd-b86b-989b84afc22a-en_US.ts | 4 + debian/control | 19 ++ debian/nymea-plugin-anel.install.in | 1 + nymea-plugins.pro | 1 + 10 files changed, 517 insertions(+) create mode 100644 anel/anel.pro create mode 100644 anel/anelpanel.cpp create mode 100644 anel/anelpanel.h create mode 100644 anel/devicepluginanel.cpp create mode 100644 anel/devicepluginanel.h create mode 100644 anel/devicepluginanel.json create mode 100644 anel/translations/7a3e5b64-20e4-42bd-b86b-989b84afc22a-en_US.ts create mode 100644 debian/nymea-plugin-anel.install.in diff --git a/anel/anel.pro b/anel/anel.pro new file mode 100644 index 00000000..b093506e --- /dev/null +++ b/anel/anel.pro @@ -0,0 +1,13 @@ +include(../plugins.pri) + +QT += network + +TARGET = $$qtLibraryTarget(nymea_devicepluginanel) + +SOURCES += \ + devicepluginanel.cpp \ + anelpanel.cpp + +HEADERS += \ + devicepluginanel.h \ + anelpanel.h diff --git a/anel/anelpanel.cpp b/anel/anelpanel.cpp new file mode 100644 index 00000000..4ba81090 --- /dev/null +++ b/anel/anelpanel.cpp @@ -0,0 +1,6 @@ +#include "anelpanel.h" + +AnelPanel::AnelPanel(const QHostAddress &hostAddress, QObject *parent) : QObject(parent) +{ + Q_UNUSED(hostAddress) +} diff --git a/anel/anelpanel.h b/anel/anelpanel.h new file mode 100644 index 00000000..373595e9 --- /dev/null +++ b/anel/anelpanel.h @@ -0,0 +1,21 @@ +#ifndef ANELPANEL_H +#define ANELPANEL_H + +#include +#include +#include + +class AnelPanel : public QObject +{ + Q_OBJECT +public: + explicit AnelPanel(const QHostAddress &hostAddress, QObject *parent = nullptr); + +signals: + +public slots: +// QUdpSocket *m_receiveSocket = nullptr; +// QUdpSocket *m_ +}; + +#endif // ANELPANEL_H diff --git a/anel/devicepluginanel.cpp b/anel/devicepluginanel.cpp new file mode 100644 index 00000000..def1774e --- /dev/null +++ b/anel/devicepluginanel.cpp @@ -0,0 +1,307 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 tasmota.html + \title ANEL Elektronik devices + \brief Plugin for ANEL Elektronik NET-PwrCtrl network controlled power sockets. + + \ingroup plugins + \ingroup nymea-plugins-maker + + This plugin allows to make use of ANEL Elektronik NET-PwrCtrl controlled powet sockets. + + See https://anel-elektronik.de/ for a detailed description of the devices. + + \chapter Plugin properties + When adding a device it will detect the type of the panel and create a gateway device and a powersocket + device for each of the available sockets on the panel. + + \quotefile plugins/deviceplugins/tasmota/devicepluginanel.json +*/ + +#include "devicepluginanel.h" +#include "plugininfo.h" +#include "plugintimer.h" + +#include +#include +#include +#include + +DevicePluginAnel::DevicePluginAnel() +{ + m_connectedStateTypeIdMap.insert(netPwrCtlDeviceClassId, netPwrCtlConnectedStateTypeId); + m_connectedStateTypeIdMap.insert(socketDeviceClassId, socketConnectedStateTypeId); + + m_nam = new QNetworkAccessManager(this); + connect(m_nam, &QNetworkAccessManager::authenticationRequired, this, [](QNetworkReply* reply, QAuthenticator *authenticator){ + qCDebug(dcAnelElektronik()) << "Auth required"; + Q_UNUSED(reply) + authenticator->setUser("admin"); + authenticator->setPassword("anel"); + }); +} + +DevicePluginAnel::~DevicePluginAnel() +{ +} + +void DevicePluginAnel::init() +{ +} + +DeviceManager::DeviceError DevicePluginAnel::discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) +{ + Q_UNUSED(deviceClassId) + Q_UNUSED(params) + + QUdpSocket *searchSocket = new QUdpSocket(this); + + // Note: This will fail, and it's not a problem, but it is required to force the socket to stick to IPv4... + searchSocket->bind(QHostAddress::AnyIPv4, 30303); + + QString discoveryString = "Durchsuchen: Wer ist da?"; + qint64 len = searchSocket->writeDatagram(discoveryString.toUtf8(), QHostAddress("255.255.255.255"), 30303); + if (len != discoveryString.length()) { + searchSocket->deleteLater(); + qCWarning(dcAnelElektronik()) << "Error sending discovery"; + return DeviceManager::DeviceErrorHardwareFailure; + } + + QTimer::singleShot(2000, this, [this, searchSocket](){ + QList descriptorList; + while(searchSocket->hasPendingDatagrams()) { + QNetworkDatagram datagram = searchSocket->receiveDatagram(); + qCDebug(dcAnelElektronik()) << "Have datagram:" << datagram.data(); + if (!datagram.data().startsWith("NET-CONTROL")) { + qCDebug(dcAnelElektronik()) << "Failed to parse discovery datagram from" << datagram.senderAddress() << datagram.data(); + continue; + } + QStringList parts = QString(datagram.data()).split("\r\n"); + if (parts.count() != 4) { + qCDebug(dcAnelElektronik()) << "Failed to parse discovery datagram from" << datagram.senderAddress() << datagram.data(); + continue; + } + qCDebug(dcAnelElektronik()) << "Found NET-CONTROL:" << datagram.senderAddress() << parts.at(2) << parts.at(3) << datagram.senderAddress().protocol(); + DeviceDescriptor d(netPwrCtlDeviceClassId, parts.at(2), datagram.senderAddress().toString()); + ParamList params; + params << Param(netPwrCtlDeviceIpAddressParamTypeId, datagram.senderAddress().toString()); + params << Param(netPwrCtlDevicePortParamTypeId, parts.at(3).toInt()); + d.setParams(params); + descriptorList << d; + } + emit devicesDiscovered(netPwrCtlDeviceClassId, descriptorList); + searchSocket->deleteLater(); + }); + return DeviceManager::DeviceErrorAsync; +} + +DeviceManager::DeviceSetupStatus DevicePluginAnel::setupDevice(Device *device) +{ + if (device->deviceClassId() == netPwrCtlDeviceClassId) { +// int sendPort = device->paramValue(netPwrCtlHomeDeviceSendPortParamTypeId).toInt(); +// int receivePort = device->paramValue(netPwrCtlHomeDeviceReceivePortParamTypeId).toInt(); + + + QNetworkRequest request; + request.setUrl(QUrl("http://" + device->paramValue(netPwrCtlDeviceIpAddressParamTypeId).toString() + ":" + device->paramValue(netPwrCtlDevicePortParamTypeId).toString() + "/strg.cfg")); + QNetworkReply *reply = m_nam->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, device, [this, device, reply](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcAnelElektronik()) << "Error fetching state for" << device->name(); + device->setStateValue(netPwrCtlConnectedStateTypeId, false); + emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure); + return; + } + device->setStateValue(netPwrCtlConnectedStateTypeId, true); + + QByteArray data = reply->readAll(); + + QStringList parts = QString(data).split(';'); + + int startIndex = parts.indexOf("end") - 58; + if (startIndex < 0 || !parts.at(startIndex + 1).startsWith("NET-CONTROL")) { + qCWarning(dcAnelElektronik()) << "Bad data from panel:" << data << "Length:" << parts.length(); + emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusFailure); + return; + } + + // At this point we're done with gathering information about the panel. Setup defintely succeeded for the gateway device + emit deviceSetupFinished(device, DeviceManager::DeviceSetupStatusSuccess); + + // If we haven't set up childs for this gateway yet, let's do it now + foreach (Device *child, myDevices()) { + if (child->parentId() == device->id()) { + // Already have childs for this panel. We're done here + return; + } + } + + // Example reply: + + // NET-PWRCTRL_04.5; // device name + // NET-CONTROL ; // hostname + // 10.10.10.132; // IP + // 255.255.255.0; // Netmask + // 10.10.10.1; // Gateway + // 00:04:A3:0B:0C:3A; // MAC + // 80; // Webcontrol port + // ; // Temp + // H; // Type + // ; // ?? (Skipped by upstream code) + + // Following fields are repeated 1 times each, one for each socket + + // Nr. 1; // Name 1 + // 1; // Stand + // 0; // Dis + // Anfangsstatus; // Info + // ; // TK + + // end; + // NET - Power Control" + + + // Lets add the child devices now + int childs = -1; + QString type = parts.at(startIndex + 8); + if (type == "H") { + childs = 3; + } else { + childs = 8; + } + + QList descriptorList; + for (int i = 0; i < childs; i++) { + QString deviceName = parts.at(startIndex + 10 + i); + DeviceDescriptor d(socketDeviceClassId, deviceName, device->name(), device->id()); + d.setParams(ParamList() << Param(socketDeviceNumberParamTypeId, i)); + descriptorList << d; + } + emit autoDevicesAppeared(socketDeviceClassId, descriptorList); + }); + + return DeviceManager::DeviceSetupStatusAsync; + } + + if (device->deviceClassId() == socketDeviceClassId) { + qCDebug(dcAnelElektronik()) << "Setting up" << device->name(); + if (!m_pollTimer) { + m_pollTimer = hardwareManager()->pluginTimerManager()->registerTimer(2); + connect(m_pollTimer, &PluginTimer::timeout, this, &DevicePluginAnel::refreshStates); + } + return DeviceManager::DeviceSetupStatusSuccess; + } + + qCWarning(dcAnelElektronik) << "Unhandled DeviceClass in setupDevice" << device->deviceClassId(); + return DeviceManager::DeviceSetupStatusFailure; +} + +void DevicePluginAnel::deviceRemoved(Device *device) +{ + qCDebug(dcAnelElektronik) << "Device removed" << device->name(); + if (myDevices().isEmpty()) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pollTimer); + m_pollTimer = nullptr; + } +} + +DeviceManager::DeviceError DevicePluginAnel::executeAction(Device *device, const Action &action) +{ + if (device->deviceClassId() == socketDeviceClassId) { + + Device *parentDevice = myDevices().findById(device->parentId()); + + if (action.actionTypeId() == socketPowerActionTypeId) { + QUrl url("http://" + parentDevice->paramValue(netPwrCtlDeviceIpAddressParamTypeId).toString() + ":" + parentDevice->paramValue(netPwrCtlDevicePortParamTypeId).toString() + "/ctrl.htm"); + QNetworkRequest request(url); + QByteArray data = QString("F%1=%2").arg(device->paramValue(socketDeviceNumberParamTypeId).toString(), action.param(socketPowerActionPowerParamTypeId).value().toBool() == true ? "1" : "0").toUtf8(); + QNetworkReply *reply = m_nam->post(request, data); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, device, [this, reply, action](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcAnelElektronik()) << "Execute action failed:" << reply->error() << reply->errorString(); + emit actionExecutionFinished(action.id(), DeviceManager::DeviceErrorHardwareNotAvailable); + } + qCDebug(dcAnelElektronik()) << "Execute action done."; + emit actionExecutionFinished(action.id(), DeviceManager::DeviceErrorNoError); + }); + return DeviceManager::DeviceErrorAsync; + } + } + return DeviceManager::DeviceErrorDeviceClassNotFound; +} + +void DevicePluginAnel::refreshStates() +{ + foreach (Device *device, myDevices()) { + if (device->deviceClassId() != netPwrCtlDeviceClassId) { + continue; + } + + QUrl url(QUrl("http://" + device->paramValue(netPwrCtlDeviceIpAddressParamTypeId).toString() + ":" + device->paramValue(netPwrCtlDevicePortParamTypeId).toString() + "/strg.cfg")); +// qCDebug(dcAnelElektronik()) << "Fetching state from:" << url.toString(); + + QNetworkRequest request; + request.setUrl(url); + QNetworkReply *reply = m_nam->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, device, [this, device, reply](){ + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcAnelElektronik()) << "Error fetching state for" << device->name(); + setConnectedState(device, false); + return; + } + QByteArray data = reply->readAll(); +// qCDebug(dcAnelElektronik()) << "States reply:" << data; + + QStringList parts = QString(data).split(';'); + int startIndex = parts.indexOf("end") - 58; + if (startIndex < 0 || !parts.at(startIndex + 1).startsWith("NET-CONTROL")) { + qCWarning(dcAnelElektronik()) << "Bad data from Panel" << device->name() << data; + // This happens sometimes as the panel replies with packets we didn't request... Just ignore it... + return; + } + setConnectedState(device, true); + + foreach (Device *child, myDevices()) { + if (child->parentId() == device->id()) { + int number = child->paramValue(socketDeviceNumberParamTypeId).toInt(); + child->setStateValue(socketPowerStateTypeId, parts.value(startIndex + 20 + number).toInt() == 1); + } + } + }); + } + +} + +void DevicePluginAnel::setConnectedState(Device *device, bool connected) +{ + device->setStateValue(m_connectedStateTypeIdMap.value(device->deviceClassId()), connected); + foreach (Device *child, myDevices()) { + if (child->parentId() == device->id()) { + child->setStateValue(m_connectedStateTypeIdMap.value(child->deviceClassId()), connected); + } + } +} diff --git a/anel/devicepluginanel.h b/anel/devicepluginanel.h new file mode 100644 index 00000000..28fd1be8 --- /dev/null +++ b/anel/devicepluginanel.h @@ -0,0 +1,63 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * 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 DEVICEPLUGINANEL_H +#define DEVICEPLUGINANEL_H + +#include "plugin/deviceplugin.h" +#include "devicemanager.h" + +#include + +#include + +class PluginTimer; + +class DevicePluginAnel: public DevicePlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginanel.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginAnel(); + ~DevicePluginAnel(); + + void init() override; + DeviceManager::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; + DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; + void deviceRemoved(Device *device) override; + DeviceManager::DeviceError executeAction(Device *device, const Action &action) override; + +private slots: + void refreshStates(); + +private: + void setConnectedState(Device *device, bool connected); + +private: + QNetworkAccessManager *m_nam = nullptr; + PluginTimer *m_pollTimer = nullptr; + + QHash m_connectedStateTypeIdMap; +}; + +#endif // DEVICEPLUGINANEL_H diff --git a/anel/devicepluginanel.json b/anel/devicepluginanel.json new file mode 100644 index 00000000..02dbe61c --- /dev/null +++ b/anel/devicepluginanel.json @@ -0,0 +1,82 @@ +{ + "name": "anelElektronik", + "displayName": "ANEL-Elektronik AG", + "id": "7a3e5b64-20e4-42bd-b86b-989b84afc22a", + "vendors": [ + { + "name": "anelElektronik", + "displayName": "ANEL-Elektronik AG", + "id": "0e0a7d31-9f6b-402f-8029-8f1b2a77f994", + "deviceClasses": [ + { + "id": "d70433ac-9738-49ca-932f-6d3e20bcc6d4", + "name": "netPwrCtl", + "displayName": "NET-PwrCtl", + "createMethods": ["user", "discovery"], + "interfaces": [ "gateway" ], + "paramTypes": [ + { + "id": "1e273e10-3ea0-4337-a221-3b8e26c6e7dc", + "name":"ipAddress", + "displayName": "IP address", + "type": "QString" + }, + { + "id": "81704e09-d283-49d1-9e3f-9c06f8b98d84", + "name": "port", + "displayName": "Web control Port", + "type": "int", + "defaultValue": 80 + } + ], + "stateTypes": [ + { + "id": "9cde6321-2abf-4a58-a1d6-c7418edb9747", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + } + ] + }, + { + "id": "9d8da004-a8a1-457f-a8ee-b86133828a49", + "name": "socket", + "displayName": "NET-PwrCtrl Socket", + "createMethods": ["auto"], + "interfaces": ["powersocket", "connectable"], + "paramTypes": [ + { + "id": "7d18f8b1-4eb8-433f-b833-14059dd190e9", + "name": "number", + "displayName": "Socket number", + "type": "int" + } + ], + "stateTypes": [ + { + "id": "e7e868a0-2de4-46ba-8ce7-87eaa4fc8e06", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "47329958-c33f-478f-b2a0-910abd150da8", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "writable": true, + "type": "bool", + "defaultValue": false + } + ] + } + ] + } + ] +} diff --git a/anel/translations/7a3e5b64-20e4-42bd-b86b-989b84afc22a-en_US.ts b/anel/translations/7a3e5b64-20e4-42bd-b86b-989b84afc22a-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/anel/translations/7a3e5b64-20e4-42bd-b86b-989b84afc22a-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/debian/control b/debian/control index 305cea50..798471e3 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,24 @@ Build-depends: libboblight-dev, Standards-Version: 3.9.3 +Package: nymea-plugin-anel +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Replaces: guh-plugin-anel +Description: nymea.io plugin for ANEL Elektronik NET-PwrCtrl power sockets + 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 ANEL Elektronik NET-PwrCtrl + network controlled power sockets. + + Package: nymea-plugin-avahimonitor Architecture: any Section: libs @@ -742,6 +760,7 @@ Depends: nymea-plugin-boblight, nymea-plugin-httpcommander, nymea-plugin-genericelements, nymea-plugin-avahimonitor, + nymea-plugin-anel, nymea-plugin-gpio, nymea-plugin-mqttclient, nymea-plugin-remotessh, diff --git a/debian/nymea-plugin-anel.install.in b/debian/nymea-plugin-anel.install.in new file mode 100644 index 00000000..b404c480 --- /dev/null +++ b/debian/nymea-plugin-anel.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginanel.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index ae13b1b4..7f4fbaf7 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -1,6 +1,7 @@ TEMPLATE = subdirs PLUGIN_DIRS = \ + anel \ avahimonitor \ awattar \ boblight \