From 2904f3a782026667c2df10b93e99ed9145813b8b Mon Sep 17 00:00:00 2001 From: Bernhard Trinnes Date: Mon, 12 Mar 2018 17:44:08 +0100 Subject: [PATCH] added keba plug-in --- keba/devicepluginkeba.cpp | 243 +++++++++++++++++++++++++++++++++++++ keba/devicepluginkeba.h | 63 ++++++++++ keba/devicepluginkeba.json | 103 ++++++++++++++++ keba/keba.pro | 9 ++ nymea-plugins.pro | 1 + 5 files changed, 419 insertions(+) create mode 100644 keba/devicepluginkeba.cpp create mode 100644 keba/devicepluginkeba.h create mode 100644 keba/devicepluginkeba.json create mode 100644 keba/keba.pro diff --git a/keba/devicepluginkeba.cpp b/keba/devicepluginkeba.cpp new file mode 100644 index 00000000..6cf2824e --- /dev/null +++ b/keba/devicepluginkeba.cpp @@ -0,0 +1,243 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016 Simon Stuerz * + * Copyright (C) 2016 Christian Stachowitz * + * * + * 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 "devicepluginkeba.h" + +#include +#include +#include +#include +#include "plugininfo.h" +#include + + +DevicePluginKeba::DevicePluginKeba() +{ + +} + +DevicePluginKeba::~DevicePluginKeba() +{ + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); +} + +void DevicePluginKeba::init() +{ + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); + connect(m_pluginTimer, &PluginTimer::timeout, this, &DevicePluginKeba::updateData); +} + +DeviceManager::DeviceSetupStatus DevicePluginKeba::setupDevice(Device *device) +{ + qCDebug(dcKebaKeContact()) << "Setting up a new device:" << device->name() << device->params(); + + if(m_kebaDevices.isEmpty()) + { + m_kebaSocket = new QUdpSocket(this); + if (!m_kebaSocket->bind(QHostAddress::AnyIPv4,7090)) { + qCWarning(dcKebaKeContact()) << "can't bind to port"; + delete m_kebaSocket; + return DeviceManager::DeviceSetupStatusFailure; + } + connect(m_kebaSocket, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams())); + qCDebug(dcKebaKeContact()) << "create keba socket"; + } + QHostAddress address = QHostAddress(device->paramValue(wallboxIpParamTypeId).toString()); + + //Check if the IP is empty + if(address.isNull()){ + return DeviceManager::DeviceSetupStatusFailure; + } + + // check if IP is already added to another keba device + if(m_kebaDevices.keys().contains(address)){ + return DeviceManager::DeviceSetupStatusFailure; + } + + m_kebaDevices.insert(address, device); + return DeviceManager::DeviceSetupStatusSuccess; +} + +void DevicePluginKeba::postSetupDevice(Device *device) +{ + qCDebug(dcKebaKeContact()) << "Post setup" << device->name(); + QByteArray datagram; + datagram.append("report 2"); + m_kebaSocket->writeDatagram(datagram.data(),datagram.size(), QHostAddress(device->paramValue(wallboxIpParamTypeId).toString()) , 7090); +} + +void DevicePluginKeba::deviceRemoved(Device *device) +{ + // Remove devices + QHostAddress address = m_kebaDevices.key(device); + m_kebaDevices.remove(address); + + if(m_kebaDevices.isEmpty()){ + m_kebaSocket->close(); + m_kebaSocket->deleteLater(); + qCDebug(dcKebaKeContact()) << "clear socket"; + } +} + +void DevicePluginKeba::updateData() +{ + foreach (QHostAddress address, m_kebaDevices.keys()) { + QByteArray datagram; + datagram.append("report 2"); + qCDebug(dcKebaKeContact()) << "datagram : " << datagram; + m_kebaSocket->writeDatagram(datagram.data(),datagram.size(), address , 7090); + //set reachable false until successful reply from device + m_kebaDevices.value(address)->setStateValue(wallboxReachableStateTypeId,false); + } +} + +DeviceManager::DeviceError DevicePluginKeba::executeAction(Device *device, const Action &action) +{ + qCDebug(dcKebaKeContact()) << "Execute action" << device->name() << action.actionTypeId().toString(); + + if (device->deviceClassId() == wallboxDeviceClassId) { + + // Print information that we are executing now the update action + qCDebug(dcKebaKeContact()) << "Execute update action" << action.id(); + + if(action.actionTypeId() == wallboxMaxCurrentActionTypeId){ + // Print information that we are executing now the update action + qCDebug(dcKebaKeContact()) << "update max current to : " << action.param(wallboxMaxCurrentStateParamTypeId).value().toString(); + QByteArray datagram; + datagram.append("curr " + QVariant(action.param(wallboxMaxCurrentStateParamTypeId).value().toInt()*1000).toString()); + qCDebug(dcKebaKeContact()) << "datagram : " << datagram; + m_kebaSocket->writeDatagram(datagram.data(),datagram.size(), QHostAddress(device->paramValue(wallboxIpParamTypeId).toString()) , 7090); + } + else if(action.actionTypeId() == wallboxOutEnableActionTypeId){ + // Print information that we are executing now the update action + qCDebug(dcKebaKeContact()) << "output enable : " << action.param(wallboxOutEnableStateParamTypeId).value().toString(); + QByteArray datagram; + if(action.param(wallboxOutEnableStateParamTypeId).value().toBool()){ + datagram.append("ena 1"); + } + else{ + datagram.append("ena 0"); + } + qCDebug(dcKebaKeContact()) << "datagram : " << datagram; + m_kebaSocket->writeDatagram(datagram.data(),datagram.size(), QHostAddress(device->paramValue(wallboxIpParamTypeId).toString()) , 7090); + } + + return DeviceManager::DeviceErrorNoError; + } + + return DeviceManager::DeviceErrorDeviceClassNotFound; +} + +void DevicePluginKeba::readPendingDatagrams() +{ + QUdpSocket *socket= qobject_cast(sender()); + + QByteArray datagram; + QHostAddress sender; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); + + qCDebug(dcKebaKeContact()) << " got command from" << sender.toString() << senderPort; + } + + if(!m_kebaDevices.keys().contains(sender)){ + qCDebug(dcKebaKeContact()) << " unknown sender:" << sender.toString() << senderPort; + return; + } + + // Convert the rawdata to a json document + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(datagram, &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcKebaKeContact()) << "Failed to parse JSON data" << datagram << ":" << error.errorString(); + return; + } + + // print the fetched data in json format to stdout + //qCDebug(dcKebaKeContact()) << qUtf8Printable(jsonDoc.toJson()); + + QVariantMap data = jsonDoc.toVariant().toMap(); + + qCDebug(dcKebaKeContact()) << "IP" << sender << "device: " << m_kebaDevices.value(sender); + + if(data.contains("ID")){ + // check if ID matches report 2 or report 3 + if(data.value("ID").toString() == "2"){ + //set reachable + m_kebaDevices.value(sender)->setStateValue(wallboxReachableStateTypeId,true); + //activity state + if(data.value("State").toString() == "0"){ + m_kebaDevices.value(sender)->setStateValue(wallboxActivityStateTypeId,"starting"); + } + else if(data.value("State").toString() == "1"){ + m_kebaDevices.value(sender)->setStateValue(wallboxActivityStateTypeId,"not ready for charging"); + } + else if(data.value("State").toString() == "2"){ + m_kebaDevices.value(sender)->setStateValue(wallboxActivityStateTypeId,"ready for charging"); + } + else if(data.value("State").toString() == "3"){ + m_kebaDevices.value(sender)->setStateValue(wallboxActivityStateTypeId,"charging"); + } + else if(data.value("State").toString() == "4"){ + m_kebaDevices.value(sender)->setStateValue(wallboxActivityStateTypeId,"error"); + } + else if(data.value("State").toString() == "5"){ + m_kebaDevices.value(sender)->setStateValue(wallboxActivityStateTypeId,"authorization rejected"); + } + // plug state + if(data.value("Plug").toString() == "0"){ + m_kebaDevices.value(sender)->setStateValue(wallboxPlugStateStateTypeId,"unplugged"); + } + else if(data.value("Plug").toString() == "1"){ + m_kebaDevices.value(sender)->setStateValue(wallboxPlugStateStateTypeId,"plugged on charging station"); + } + else if(data.value("Plug").toString() == "3"){ + m_kebaDevices.value(sender)->setStateValue(wallboxPlugStateStateTypeId,"locked plug on charging station"); + } + else if(data.value("Plug").toString() == "5"){ + m_kebaDevices.value(sender)->setStateValue(wallboxPlugStateStateTypeId,"plugged on charging station and vehicle"); + } + else if(data.value("Plug").toString() == "7"){ + m_kebaDevices.value(sender)->setStateValue(wallboxPlugStateStateTypeId,"locked plug on charging station and vehicle"); + } + //maximum current setting + m_kebaDevices.value(sender)->setStateValue(wallboxMaxCurrentStateTypeId,data.value("Curr user").toInt()/1000); + //output setting + m_kebaDevices.value(sender)->setStateValue(wallboxOutEnableStateTypeId,data.value("Enable user").toBool()); + + //request next report + QByteArray datagram; + datagram.append("report 3"); + qCDebug(dcKebaKeContact()) << "datagram : " << datagram; + socket->writeDatagram(datagram.data(),datagram.size(), QHostAddress(m_kebaDevices.value(sender)->paramValue(wallboxIpParamTypeId).toString()) , 7090); + } + else if(data.value("ID").toString() == "3"){ + //power of current charging session + m_kebaDevices.value(sender)->setStateValue(wallboxPowerStateTypeId,data.value("E pres").toInt() / 1000); + //current phase 1 + m_kebaDevices.value(sender)->setStateValue(wallboxCurrentStateTypeId,data.value("I1").toInt() * 1000); + } + } + +} diff --git a/keba/devicepluginkeba.h b/keba/devicepluginkeba.h new file mode 100644 index 00000000..d58442a6 --- /dev/null +++ b/keba/devicepluginkeba.h @@ -0,0 +1,63 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2016 Simon Stuerz * + * Copyright (C) 2016 Christian Stachowitz * + * * + * 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 DEVICEPLUGINKEBA_H +#define DEVICEPLUGINKEBA_H + +#include "plugin/deviceplugin.h" +#include "devicemanager.h" +#include "plugintimer.h" + +#include +#include +#include + +class DevicePluginKeba : public DevicePlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginkeba.json") + Q_INTERFACES(DevicePlugin) + +public: + explicit DevicePluginKeba(); + ~DevicePluginKeba(); + + void init() override; + DeviceManager::DeviceSetupStatus setupDevice(Device *device) override; + + void postSetupDevice(Device* device) override; + void deviceRemoved(Device* device) override; + + DeviceManager::DeviceError executeAction(Device *device, const Action &action) override; + void updateData(); + +private: + PluginTimer *m_pluginTimer = nullptr; + QHash m_kebaDevices; + QUdpSocket *m_kebaSocket; + +private slots: + void readPendingDatagrams(); + +}; + +#endif // DEVICEPLUGINKEBA_H diff --git a/keba/devicepluginkeba.json b/keba/devicepluginkeba.json new file mode 100644 index 00000000..1eebfcae --- /dev/null +++ b/keba/devicepluginkeba.json @@ -0,0 +1,103 @@ +{ + "displayName": "Keba KeContact", + "name": "KebaKeContact", + "id": "9142b09f-30a9-43d0-9ede-2f8debe075ac", + "vendors": [ + { + "id": "f7cda40b-829a-4675-abaa-485697430f5f", + "displayName": "Keba", + "name": "keba", + "deviceClasses": [ + { + "id": "900dacec-cae7-4a37-95ba-501846368ea2", + "name": "wallbox", + "displayName": "Keba KeContact P30", + "deviceIcon": "Energy", + "createMethods": ["user"], + "interfaces": [], + "criticalStateTypeId": "b1a574a6-46b6-44ea-a0bb-9b4de3198967", + "basicTags": [ + "Energy" + ], + "paramTypes":[ + { + "id": "730cd3d3-5f0e-4028-a8c2-ced7574f13f3", + "name": "ip", + "displayName": "IP Address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue":"0.0.0.0" + } + ], + "stateTypes": [ + { + "id": "539e5602-6dd9-465d-9705-3bb59bcf8982", + "name": "activity", + "displayName": "Activity", + "displayNameEvent": "Activity changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "3b4d29f3-3101-47ad-90fd-269b6348783b", + "name": "plugState", + "displayName": "Plug State", + "displayNameEvent": "Plug State changed", + "type": "QString", + "defaultValue": "-" + }, + { + "id": "a29c1748-fe97-4830-a56e-e1cc4e618385", + "name": "current", + "displayName": "Current", + "displayNameEvent": "Current changed", + "type": "int", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "593656f0-babf-4308-8767-68f34e10fb15", + "name": "maxCurrent", + "displayName": "maximal Current", + "displayNameEvent": "Maximal Current changed", + "displayNameAction": "Set maximal current", + "type": "int", + "unit": "Ampere", + "defaultValue": 6, + "minValue": 6, + "maxValue": 63, + "writable": true + }, + { + "id": "e8f069ca-7fa7-4568-8d4c-165f6d048720", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "type": "int", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "0cd5396a-bc41-4c8f-b037-db10991a76c7", + "name": "outEnable", + "displayName": "Output", + "displayNameEvent": "Output Enable changed", + "displayNameAction": "Set Output", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "b1a574a6-46b6-44ea-a0bb-9b4de3198967", + "name": "reachable", + "displayName": "reachable", + "displayNameEvent": "Device Reachable changed", + "type": "bool", + "defaultValue": false + } + ] + } + ] + } + ] +} diff --git a/keba/keba.pro b/keba/keba.pro new file mode 100644 index 00000000..edac80c1 --- /dev/null +++ b/keba/keba.pro @@ -0,0 +1,9 @@ +include(../plugins.pri) + +TARGET = $$qtLibraryTarget(nymea_devicepluginkeba) + +SOURCES += \ + devicepluginkeba.cpp \ + +HEADERS += \ + devicepluginkeba.h \ diff --git a/nymea-plugins.pro b/nymea-plugins.pro index cbea44b2..e1d7c09d 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -33,6 +33,7 @@ PLUGIN_DIRS = \ gpio \ snapd \ simulation \ + keba \ CONFIG+=all