New Plugin: ANEL Elektronik NET-PwrCtrl

master
Michael Zanetti 2019-01-02 22:49:47 +01:00
parent b42740de52
commit 6640316cf3
10 changed files with 517 additions and 0 deletions

13
anel/anel.pro Normal file
View File

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

6
anel/anelpanel.cpp Normal file
View File

@ -0,0 +1,6 @@
#include "anelpanel.h"
AnelPanel::AnelPanel(const QHostAddress &hostAddress, QObject *parent) : QObject(parent)
{
Q_UNUSED(hostAddress)
}

21
anel/anelpanel.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef ANELPANEL_H
#define ANELPANEL_H
#include <QObject>
#include <QHostAddress>
#include <QUdpSocket>
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

307
anel/devicepluginanel.cpp Normal file
View File

@ -0,0 +1,307 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 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 <QNetworkDatagram>
#include <network/networkaccessmanager.h>
#include <QNetworkReply>
#include <QAuthenticator>
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 &params)
{
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<DeviceDescriptor> 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<DeviceDescriptor> 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);
}
}
}

63
anel/devicepluginanel.h Normal file
View File

@ -0,0 +1,63 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* 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 DEVICEPLUGINANEL_H
#define DEVICEPLUGINANEL_H
#include "plugin/deviceplugin.h"
#include "devicemanager.h"
#include <QUdpSocket>
#include <QNetworkAccessManager>
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 &params) 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<DeviceClassId, StateTypeId> m_connectedStateTypeIdMap;
};
#endif // DEVICEPLUGINANEL_H

View File

@ -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
}
]
}
]
}
]
}

View File

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

19
debian/control vendored
View File

@ -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,

1
debian/nymea-plugin-anel.install.in vendored Normal file
View File

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

View File

@ -1,6 +1,7 @@
TEMPLATE = subdirs
PLUGIN_DIRS = \
anel \
avahimonitor \
awattar \
boblight \