diff --git a/debian/control b/debian/control index 71622413..378525e1 100644 --- a/debian/control +++ b/debian/control @@ -901,6 +901,7 @@ Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, nymea-plugins-translations, + nmap, Replaces: guh-plugin-keba Description: nymea.io plugin for keba The nymea daemon is a plugin based IoT (Internet of Things) server. The diff --git a/keba/README.md b/keba/README.md index f436ea21..87bdbebb 100644 --- a/keba/README.md +++ b/keba/README.md @@ -1 +1,21 @@ # Keba Wallbox + +This plugin allows to control Keba KeContact EV-Charging stations. + +## Supported Things + +* KeContact + * Enable/disable the charging stations + * Set maximum charging current + * Get all informations e.g. voltage, current ... + * Print messages on the display + +## Requirments + +* nymea and the wallbox are required to be in the same network. +* Port 7090 must not be blocked by a firewall or router. +* The package "nymea-plugin-keba" must be installed. + +## More + +https://www.keba.com/en/emobility/products/product-overview/product_overview diff --git a/keba/discovery.cpp b/keba/discovery.cpp new file mode 100644 index 00000000..070f64e1 --- /dev/null +++ b/keba/discovery.cpp @@ -0,0 +1,271 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "discovery.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include +#include + +Discovery::Discovery(QObject *parent) : QObject(parent) +{ + connect(&m_timeoutTimer, &QTimer::timeout, this, &Discovery::onTimeout); +} + +void Discovery::discoverHosts(int timeout) +{ + if (isRunning()) { + qCWarning(dcKebaKeContact()) << "Discovery already running. Cannot start twice."; + return; + } + m_timeoutTimer.start(timeout * 1000); + + foreach (const QString &target, getDefaultTargets()) { + QProcess *discoveryProcess = new QProcess(this); + m_discoveryProcesses.append(discoveryProcess); + connect(discoveryProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + + QStringList arguments; + arguments << "-oX" << "-" << "-n" << "-sn"; + arguments << target; + + qCDebug(dcKebaKeContact()) << "Scanning network:" << "nmap" << arguments.join(" "); + discoveryProcess->start(QStringLiteral("nmap"), arguments); + } +} + +void Discovery::abort() +{ + foreach (QProcess *discoveryProcess, m_discoveryProcesses) { + if (discoveryProcess->state() == QProcess::Running) { + qCDebug(dcKebaKeContact()) << "Kill running discovery process"; + discoveryProcess->terminate(); + discoveryProcess->waitForFinished(5000); + } + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + qDeleteAll(m_scanResults); + m_scanResults.clear(); +} + +bool Discovery::isRunning() const +{ + return !m_discoveryProcesses.isEmpty() || !m_pendingArpLookups.isEmpty() || !m_pendingNameLookups.isEmpty(); +} + +void Discovery::discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *discoveryProcess = static_cast(sender()); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcKebaKeContact()) << "Nmap error failed. Is nmap installed correctly?"; + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + finishDiscovery(); + return; + } + + QByteArray data = discoveryProcess->readAll(); + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + + QXmlStreamReader reader(data); + + int foundHosts = 0; + + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if(token == QXmlStreamReader::StartDocument) + continue; + + if(token == QXmlStreamReader::StartElement && reader.name() == "host") { + bool isUp = false; + QString address; + QString macAddress; + QString vendor; + while (!reader.atEnd() && !reader.hasError() && !(token == QXmlStreamReader::EndElement && reader.name() == "host")) { + token = reader.readNext(); + + if (reader.name() == "address") { + QString addr = reader.attributes().value("addr").toString(); + QString type = reader.attributes().value("addrtype").toString(); + if (type == "ipv4" && !addr.isEmpty()) { + address = addr; + } else if (type == "mac") { + macAddress = addr; + vendor = reader.attributes().value("vendor").toString(); + } + } + + if (reader.name() == "status") { + QString state = reader.attributes().value("state").toString(); + if (!state.isEmpty()) + isUp = state == "up"; + } + } + + if (isUp) { + foundHosts++; + qCDebug(dcKebaKeContact()) << "Have host:" << address; + + Host *host = new Host(); + host->setAddress(address); + + if (!macAddress.isEmpty()) { + host->setMacAddress(macAddress); + } else { + QProcess *arpLookup = new QProcess(this); + m_pendingArpLookups.insert(arpLookup, host); + connect(arpLookup, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(arpLookupDone(int,QProcess::ExitStatus))); + arpLookup->start("arp", {"-vn"}); + } + + host->setHostName(vendor); + QHostInfo::lookupHost(address, this, SLOT(hostLookupDone(QHostInfo))); + m_pendingNameLookups.insert(address, host); + + m_scanResults.append(host); + } + } + } + + if (foundHosts == 0 && m_discoveryProcesses.isEmpty()) { + qCDebug(dcKebaKeContact()) << "Network scan successful but no hosts found in this network"; + finishDiscovery(); + } +} + +void Discovery::hostLookupDone(const QHostInfo &info) +{ + Host *host = m_pendingNameLookups.take(info.addresses().first().toString()); + if (!host) { + // Probably aborted... + return; + } + if (info.error() != QHostInfo::NoError) { + qWarning(dcKebaKeContact()) << "Host lookup failed:" << info.errorString(); + } + if (host->hostName().isEmpty() || info.hostName() != host->address()) { + host->setHostName(info.hostName()); + } + + finishDiscovery(); +} + +void Discovery::arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *p = static_cast(sender()); + p->deleteLater(); + + Host *host = m_pendingArpLookups.take(p); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcKebaKeContact()) << "ARP lookup process failed for host" << host->address(); + finishDiscovery(); + return; + } + + QString data = QString::fromLatin1(p->readAll()); + foreach (QString line, data.split('\n')) { + line.replace(QRegExp("[ ]{1,}"), " "); + QStringList parts = line.split(" "); + if (parts.count() >= 3 && parts.first() == host->address() && parts.at(1) == "ether") { + host->setMacAddress(parts.at(2)); + break; + } + } + finishDiscovery(); +} + +void Discovery::onTimeout() +{ + qWarning(dcKebaKeContact()) << "Timeout hit. Stopping discovery"; + while (!m_discoveryProcesses.isEmpty()) { + QProcess *discoveryProcess = m_discoveryProcesses.takeFirst(); + disconnect(this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + discoveryProcess->terminate(); + delete discoveryProcess; + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + m_scanResults.removeAll(m_pendingArpLookups.value(p)); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + finishDiscovery(); +} + +QStringList Discovery::getDefaultTargets() +{ + QStringList targets; + foreach (const QHostAddress &interface, QNetworkInterface::allAddresses()) { + if (!interface.isLoopback() && interface.scopeId().isEmpty() && interface.protocol() == QAbstractSocket::IPv4Protocol) { + QPair pair = QHostAddress::parseSubnet(interface.toString() + "/24"); + QString newTarget = QString("%1/%2").arg(pair.first.toString()).arg(pair.second); + if (!targets.contains(newTarget)) { + targets.append(newTarget); + } + } + } + return targets; +} + +void Discovery::finishDiscovery() +{ + if (m_discoveryProcesses.count() > 0 || m_pendingNameLookups.count() > 0 || m_pendingArpLookups.count() > 0) { + // Still busy... + return; + } + + QList hosts; + foreach (Host *host, m_scanResults) { + if (!host->macAddress().isEmpty()) { + hosts.append(*host); + } + } + qDeleteAll(m_scanResults); + m_scanResults.clear(); + + qCDebug(dcKebaKeContact()) << "Emitting device discovered for" << hosts.count() << "devices"; + m_timeoutTimer.stop(); + emit finished(hosts); +} + diff --git a/keba/discovery.h b/keba/discovery.h new file mode 100644 index 00000000..02284c38 --- /dev/null +++ b/keba/discovery.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DISCOVERY_H +#define DISCOVERY_H + +#include +#include +#include +#include + +#include "host.h" + +class Discovery : public QObject +{ + Q_OBJECT +public: + explicit Discovery(QObject *parent = nullptr); + + void discoverHosts(int timeout); + void abort(); + bool isRunning() const; + +signals: + void finished(const QList &hosts); + +private: + QStringList getDefaultTargets(); + + void finishDiscovery(); + +private slots: + void discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus); + void hostLookupDone(const QHostInfo &info); + void arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus); + void onTimeout(); + +private: + QList m_discoveryProcesses; + QTimer m_timeoutTimer; + + QHash m_pendingArpLookups; + QHash m_pendingNameLookups; + QList m_scanResults; +}; + +#endif // DISCOVERY_H + diff --git a/keba/host.cpp b/keba/host.cpp new file mode 100644 index 00000000..f1e80a53 --- /dev/null +++ b/keba/host.cpp @@ -0,0 +1,94 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "host.h" + +Host::Host() +{ + qRegisterMetaType(); + qRegisterMetaType >(); +} + +QString Host::macAddress() const +{ + return m_macAddress; +} + +void Host::setMacAddress(const QString &macAddress) +{ + m_macAddress = macAddress; +} + +QString Host::hostName() const +{ + return m_hostName; +} + +void Host::setHostName(const QString &hostName) +{ + m_hostName = hostName; +} + +QString Host::address() const +{ + return m_address; +} + +void Host::setAddress(const QString &address) +{ + m_address = address; +} + +void Host::seen() +{ + m_lastSeenTime = QDateTime::currentDateTime(); +} + +QDateTime Host::lastSeenTime() const +{ + return m_lastSeenTime; +} + +bool Host::reachable() const +{ + return m_reachable; +} + +void Host::setReachable(bool reachable) +{ + m_reachable = reachable; +} + +QDebug operator<<(QDebug dbg, const Host &host) +{ + dbg.nospace() << "Host(" << host.macAddress() << "," << host.hostName() << ", " << host.address() << ", " << (host.reachable() ? "up" : "down") << ")"; + return dbg.space(); +} + diff --git a/keba/host.h b/keba/host.h new file mode 100644 index 00000000..8fe28a23 --- /dev/null +++ b/keba/host.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef HOST_H +#define HOST_H + +#include +#include +#include + +class Host +{ +public: + Host(); + + QString macAddress() const; + void setMacAddress(const QString &macAddress); + + QString hostName() const; + void setHostName(const QString &hostName); + + QString address() const; + void setAddress(const QString &address); + + void seen(); + QDateTime lastSeenTime() const; + + bool reachable() const; + void setReachable(bool reachable); + +private: + QString m_macAddress; + QString m_hostName; + QString m_address; + QDateTime m_lastSeenTime; + bool m_reachable; +}; +Q_DECLARE_METATYPE(Host) + +QDebug operator<<(QDebug dbg, const Host &host); + +#endif // HOST_H + diff --git a/keba/integrationpluginkeba.cpp b/keba/integrationpluginkeba.cpp index 5fb231b4..381fccd1 100644 --- a/keba/integrationpluginkeba.cpp +++ b/keba/integrationpluginkeba.cpp @@ -29,28 +29,51 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationpluginkeba.h" +#include "plugininfo.h" #include #include #include -#include -#include "plugininfo.h" #include +#include IntegrationPluginKeba::IntegrationPluginKeba() { } -IntegrationPluginKeba::~IntegrationPluginKeba() +void IntegrationPluginKeba::discoverThings(ThingDiscoveryInfo *info) { - hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); -} + if (info->thingClassId() == wallboxThingClassId) { + Discovery *discovery = new Discovery(info); + discovery->discoverHosts(25); -void IntegrationPluginKeba::init() -{ - m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); - connect(m_pluginTimer, &PluginTimer::timeout, this, &IntegrationPluginKeba::updateData); + connect(discovery, &Discovery::finished, info, [this, info](const QList &hosts) { + qCDebug(dcKebaKeContact()) << "Discovery finished. Found" << hosts.count() << "devices"; + foreach (const Host &host, hosts) { + if (!host.hostName().contains("keba", Qt::CaseSensitivity::CaseInsensitive)) + continue; + + ThingDescriptor descriptor(wallboxThingClassId, "Wallbox", host.address() + " (" + host.macAddress() + ")"); + + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(wallboxThingMacAddressParamTypeId).toString() == host.macAddress()) { + descriptor.setThingId(existingThing->id()); + break; + } + } + ParamList params; + params << Param(wallboxThingMacAddressParamTypeId, host.macAddress()); + params << Param(wallboxThingIpAddressParamTypeId, host.address()); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + info->finish(Thing::ThingErrorNoError); + }); + } else { + qCWarning(dcKebaKeContact()) << "Discover device, unhandled device class" << info->thingClassId(); + info->finish(Thing::ThingErrorThingClassNotFound); + } } void IntegrationPluginKeba::setupThing(ThingSetupInfo *info) @@ -59,67 +82,245 @@ void IntegrationPluginKeba::setupThing(ThingSetupInfo *info) qCDebug(dcKebaKeContact()) << "Setting up a new thing:" << thing->name() << thing->params(); - if(m_kebaDevices.isEmpty()) { - m_kebaSocket = new QUdpSocket(this); - if (!m_kebaSocket->bind(QHostAddress::AnyIPv4, 7090)) { + if (thing->thingClassId() == wallboxThingClassId) { + + QHostAddress address = QHostAddress(thing->paramValue(wallboxThingIpAddressParamTypeId).toString()); + KeContact *keba = new KeContact(address, this); + connect(keba, &KeContact::connectionChanged, this, &IntegrationPluginKeba::onConnectionChanged); + connect(keba, &KeContact::commandExecuted, this, &IntegrationPluginKeba::onCommandExecuted); + connect(keba, &KeContact::reportOneReceived, this, &IntegrationPluginKeba::onReportOneReceived); + connect(keba, &KeContact::reportTwoReceived, this, &IntegrationPluginKeba::onReportTwoReceived); + connect(keba, &KeContact::reportThreeReceived, this, &IntegrationPluginKeba::onReportThreeReceived); + connect(keba, &KeContact::broadcastReceived, this, &IntegrationPluginKeba::onBroadcastReceived); + if (!keba->init()){ qCWarning(dcKebaKeContact()) << "Cannot bind to port" << 7090; - delete m_kebaSocket; - //: Error setting up thing + keba->deleteLater(); return info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Error opening network port.")); } - connect(m_kebaSocket, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams())); - qCDebug(dcKebaKeContact()) << "Create keba socket"; + + ThingId id = thing->id(); + m_kebaDevices.insert(id, keba); + m_asyncSetup.insert(keba, info); + keba->getReport1(); + connect(info, &ThingSetupInfo::aborted, this, [id, keba, this]{ + m_asyncSetup.remove(keba); + m_kebaDevices.remove(id); + keba->deleteLater(); + }); + } else { + qCWarning(dcKebaKeContact()) << "setupDevice, unhandled device class" << thing->thingClass(); + info->finish(Thing::ThingErrorThingClassNotFound); } - - QHostAddress address = QHostAddress(thing->paramValue(wallboxThingIpParamTypeId).toString()); - - //Check if the IP is empty - if (address.isNull()) { - delete m_kebaSocket; - //: Error setting up thing - return info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("The provided IP adress is not valid.")); - } - - // check if IP is already added to another keba thing - if(m_kebaDevices.keys().contains(address)){ - //: Error setting up thing - return info->finish(Thing::ThingErrorInvalidParameter, QT_TR_NOOP("Device with IP adress %1 is already added in the system.")); - } - - m_kebaDevices.insert(address, thing); - info->finish(Thing::ThingErrorNoError); } void IntegrationPluginKeba::postSetupThing(Thing *thing) { qCDebug(dcKebaKeContact()) << "Post setup" << thing->name(); - QByteArray datagram; - datagram.append("report 2"); - m_kebaSocket->writeDatagram(datagram.data(), datagram.size(), QHostAddress(thing->paramValue(wallboxThingIpParamTypeId).toString()), 7090); + KeContact *keba = m_kebaDevices.value(thing->id()); + if (!keba) { + return; + } + keba->getReport2(); + keba->getReport3(); + + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); + connect(m_pluginTimer, &PluginTimer::timeout, this, &IntegrationPluginKeba::updateData); + } } void IntegrationPluginKeba::thingRemoved(Thing *thing) -{ - // Remove devices - QHostAddress address = m_kebaDevices.key(thing); - m_kebaDevices.remove(address); +{ + if (thing->thingClassId() == wallboxThingClassId) { + KeContact *keba = m_kebaDevices.take(thing->id()); + keba->deleteLater(); + } - if(m_kebaDevices.isEmpty()){ - m_kebaSocket->close(); - m_kebaSocket->deleteLater(); - qCDebug(dcKebaKeContact()) << "clear socket"; + if (myThings().empty()) { + // last device has been removed the plug in timer can be stopped again + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; } } void IntegrationPluginKeba::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 thing - m_kebaDevices.value(address)->setStateValue(wallboxReachableStateTypeId,false); + foreach (KeContact *keba, m_kebaDevices) { + keba->getReport2(); + keba->getReport3(); + } + + foreach (Thing *thing, myThings().filterByThingClassId(wallboxThingClassId)) { + if (m_chargingSessionStartTime.contains(thing->id())) { + QDateTime startTime = m_chargingSessionStartTime.value(thing->id()); + + QTimeZone tz = QTimeZone(QTimeZone::systemTimeZoneId()); + QDateTime currentTime = QDateTime::currentDateTime().toTimeZone(tz); + + int minutes = (currentTime.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch())/60000; + thing->setStateValue(wallboxSessionTimeStateTypeId, minutes); + } else { + thing->setStateValue(wallboxSessionTimeStateTypeId, 0); + } + } +} + +void IntegrationPluginKeba::setDeviceState(Thing *thing, KeContact::State state) +{ + switch (state) { + case KeContact::StateStarting: + thing->setStateValue(wallboxActivityStateTypeId, "Starting"); + break; + case KeContact::StateNotReady: + thing->setStateValue(wallboxActivityStateTypeId, "Not ready for charging"); + break; + case KeContact::StateReady: + thing->setStateValue(wallboxActivityStateTypeId, "Ready for charging"); + break; + case KeContact::StateCharging: + thing->setStateValue(wallboxActivityStateTypeId, "Charging"); + break; + case KeContact::StateError: + thing->setStateValue(wallboxActivityStateTypeId, "Error"); + break; + case KeContact::StateAuthorizationRejected: + thing->setStateValue(wallboxActivityStateTypeId, "Authorization rejected"); + break; + } + + if (state == KeContact::StateCharging) { + //Set charging session + QTimeZone tz = QTimeZone(QTimeZone::systemTimeZoneId()); + QDateTime startedChargingSession = QDateTime::currentDateTime().toTimeZone(tz); + m_chargingSessionStartTime.insert(thing->id(), startedChargingSession); + } else { + m_chargingSessionStartTime.remove(thing->id()); + thing->setStateValue(wallboxSessionTimeStateTypeId, 0); + } +} + +void IntegrationPluginKeba::setDevicePlugState(Thing *thing, KeContact::PlugState plugState) +{ + switch (plugState) { + case KeContact::PlugStateUnplugged: + thing->setStateValue(wallboxPlugStateStateTypeId, "Unplugged"); + break; + case KeContact::PlugStatePluggedOnChargingStation: + thing->setStateValue(wallboxPlugStateStateTypeId, "Plugged in charging station"); + break; + case KeContact::PlugStatePluggedOnChargingStationAndPluggedOnEV: + thing->setStateValue(wallboxPlugStateStateTypeId, "Plugged in on EV"); + break; + case KeContact::PlugStatePluggedOnChargingStationAndPlugLocked: + thing->setStateValue(wallboxPlugStateStateTypeId, "Plugged in and locked"); + break; + case KeContact::PlugStatePluggedOnChargingStationAndPlugLockedAndPluggedOnEV: + thing->setStateValue(wallboxPlugStateStateTypeId, "Plugged in on EV and locked"); + break; + } +} + +void IntegrationPluginKeba::onConnectionChanged(bool status) +{ + KeContact *keba = static_cast(sender()); + Thing *thing = myThings().findById(m_kebaDevices.key(keba)); + if (!thing) { + qCWarning(dcKebaKeContact()) << "On connection changed: missing device object"; + return; + } + thing->setStateValue(wallboxConnectedStateTypeId, status); + if (!status) { + //TODO start rediscovery + } +} + +void IntegrationPluginKeba::onCommandExecuted(QUuid requestId, bool success) +{ + updateData(); + if (m_asyncActions.contains(requestId)) { + KeContact *keba = static_cast(sender()); + Thing *thing = myThings().findById(m_kebaDevices.key(keba)); + if (!thing) { + qCWarning(dcKebaKeContact()) << "On command executed: missing device object"; + return; + } + ThingActionInfo *info = m_asyncActions.take(requestId); + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + } +} + +void IntegrationPluginKeba::onReportOneReceived(const KeContact::ReportOne &reportOne) +{ + Q_UNUSED(reportOne); + KeContact *keba = static_cast(sender()); + if (m_asyncSetup.contains(keba)) { + ThingSetupInfo *info = m_asyncSetup.value(keba); + info->finish(Thing::ThingErrorNoError); + } else { + qCDebug(dcKebaKeContact()) << "Report one received without an associated async setup"; + } +} + +void IntegrationPluginKeba::onReportTwoReceived(const KeContact::ReportTwo &reportTwo) +{ + KeContact *keba = static_cast(sender()); + Thing *thing = myThings().findById(m_kebaDevices.key(keba)); + if (!thing) + return; + + thing->setStateValue(wallboxPowerStateTypeId, reportTwo.enableUser); + thing->setStateValue(wallboxMaxChargingCurrentPercentStateTypeId, reportTwo.MaxCurrentPercentage); + + setDeviceState(thing, reportTwo.state); + setDevicePlugState(thing, reportTwo.plugState); +} + +void IntegrationPluginKeba::onReportThreeReceived(const KeContact::ReportThree &reportThree) +{ + KeContact *keba = static_cast(sender()); + Thing *thing = myThings().findById(m_kebaDevices.key(keba)); + if (!thing) + return; + + thing->setStateValue(wallboxI1EventTypeId, reportThree.CurrentPhase1); + thing->setStateValue(wallboxI2EventTypeId, reportThree.CurrentPhase2); + thing->setStateValue(wallboxI3EventTypeId, reportThree.CurrentPhase3); + thing->setStateValue(wallboxU1EventTypeId, reportThree.VoltagePhase1); + thing->setStateValue(wallboxU2EventTypeId, reportThree.VoltagePhase2); + thing->setStateValue(wallboxU3EventTypeId, reportThree.VoltagePhase3); + thing->setStateValue(wallboxPStateTypeId, reportThree.Power); + thing->setStateValue(wallboxEPStateTypeId, reportThree.EnergySession); + thing->setStateValue(wallboxTotalEnergyConsumedStateTypeId, reportThree.EnergyTotal); +} + +void IntegrationPluginKeba::onBroadcastReceived(KeContact::BroadcastType type, const QVariant &content) +{ + KeContact *keba = static_cast(sender()); + Thing *thing = myThings().findById(m_kebaDevices.key(keba)); + if (!thing) + return; + + switch (type) { + case KeContact::BroadcastTypePlug: + setDevicePlugState(thing, KeContact::PlugState(content.toInt())); + break; + case KeContact::BroadcastTypeInput: + break; + case KeContact::BroadcastTypeEPres: + thing->setStateValue(wallboxEPStateTypeId, content.toInt()); + break; + case KeContact::BroadcastTypeState: + setDeviceState(thing, KeContact::State(content.toInt())); + break; + case KeContact::BroadcastTypeMaxCurr: + thing->setStateValue(wallboxMaxChargingCurrentStateTypeId, content.toInt()); + break; + case KeContact::BroadcastTypeEnableSys: + break; } } @@ -128,133 +329,35 @@ void IntegrationPluginKeba::executeAction(ThingActionInfo *info) Thing *thing = info->thing(); Action action = info->action(); - qCDebug(dcKebaKeContact()) << "Execute action" << thing->name() << action.actionTypeId().toString(); - if (thing->thingClassId() == wallboxThingClassId) { - - // 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(wallboxMaxCurrentActionMaxCurrentParamTypeId).value().toString(); - QByteArray datagram; - datagram.append("curr " + QVariant(action.param(wallboxMaxCurrentActionMaxCurrentParamTypeId).value().toInt()*1000).toString()); - qCDebug(dcKebaKeContact()) << "Datagram : " << datagram; - m_kebaSocket->writeDatagram(datagram.data(),datagram.size(), QHostAddress(thing->paramValue(wallboxThingIpParamTypeId).toString()) , 7090); - } - else if(action.actionTypeId() == wallboxOutEnableActionTypeId){ - // Print information that we are executing now the update action - qCDebug(dcKebaKeContact()) << "output enable : " << action.param(wallboxOutEnableActionOutEnableParamTypeId).value().toString(); - QByteArray datagram; - if(action.param(wallboxOutEnableActionOutEnableParamTypeId).value().toBool()){ - datagram.append("ena 1"); - } - else{ - datagram.append("ena 0"); - } - qCDebug(dcKebaKeContact()) << "Datagram : " << datagram; - m_kebaSocket->writeDatagram(datagram.data(),datagram.size(), QHostAddress(thing->paramValue(wallboxThingIpParamTypeId).toString()) , 7090); + KeContact *keba = m_kebaDevices.value(thing->id()); + if (!keba) { + qCWarning(dcKebaKeContact()) << "Device not properly initialized, Keba object missing"; + return info->finish(Thing::ThingErrorHardwareNotAvailable); } - return info->finish(Thing::ThingErrorNoError); + if(action.actionTypeId() == wallboxMaxChargingCurrentActionTypeId){ + int milliAmpere = action.param(wallboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId).value().toInt(); + QUuid requestId = keba->setMaxAmpere(milliAmpere); + m_asyncActions.insert(requestId, info); + connect(info, &ThingActionInfo::aborted, this, [requestId, this]{m_asyncActions.remove(requestId);}); + + } else if(action.actionTypeId() == wallboxPowerActionTypeId){ + QUuid requestId = keba->enableOutput(action.param(wallboxPowerActionTypeId).value().toBool()); + m_asyncActions.insert(requestId, info); + connect(info, &ThingActionInfo::aborted, this, [requestId, this]{m_asyncActions.remove(requestId);}); + + } else if(action.actionTypeId() == wallboxDisplayActionTypeId){ + QUuid requestId = keba->displayMessage(action.param(wallboxDisplayActionMessageParamTypeId).value().toByteArray()); + m_asyncActions.insert(requestId, info); + connect(info, &ThingActionInfo::aborted, this, [requestId, this]{m_asyncActions.remove(requestId);}); + + } else { + qCWarning(dcKebaKeContact()) << "Unhandled ActionTypeId:" << action.actionTypeId(); + info->finish(Thing::ThingErrorActionTypeNotFound); + } + } else { + qCWarning(dcKebaKeContact()) << "Execute action, unhandled device class" << thing->thingClass(); + info->finish(Thing::ThingErrorThingClassNotFound); } - - info->finish(Thing::ThingErrorThingClassNotFound); -} - -void IntegrationPluginKeba::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 << "thing: " << 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(wallboxThingIpParamTypeId).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/integrationpluginkeba.h b/keba/integrationpluginkeba.h index 768b916d..ecbc6ec6 100644 --- a/keba/integrationpluginkeba.h +++ b/keba/integrationpluginkeba.h @@ -33,10 +33,14 @@ #include "integrations/integrationplugin.h" #include "plugintimer.h" +#include "kecontact.h" +#include "discovery.h" +#include "host.h" #include #include #include +#include class IntegrationPluginKeba : public IntegrationPlugin { @@ -47,9 +51,8 @@ class IntegrationPluginKeba : public IntegrationPlugin public: explicit IntegrationPluginKeba(); - ~IntegrationPluginKeba(); - void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing* thing) override; @@ -60,12 +63,21 @@ public: private: PluginTimer *m_pluginTimer = nullptr; - QHash m_kebaDevices; - QUdpSocket *m_kebaSocket; + QHash m_kebaDevices; + QHash m_asyncSetup; + QHash m_asyncActions; + QHash m_chargingSessionStartTime; + + void setDeviceState(Thing *device, KeContact::State state); + void setDevicePlugState(Thing *device, KeContact::PlugState plugState); private slots: - void readPendingDatagrams(); - + void onConnectionChanged(bool status); + void onCommandExecuted(QUuid requestId, bool success); + void onReportOneReceived(const KeContact::ReportOne &reportOne); + void onReportTwoReceived(const KeContact::ReportTwo &reportTwo); + void onReportThreeReceived(const KeContact::ReportThree &reportThree); + void onBroadcastReceived(KeContact::BroadcastType type, const QVariant &content); }; #endif // INTEGRATIONPLUGINKEBA_H diff --git a/keba/integrationpluginkeba.json b/keba/integrationpluginkeba.json index f602c29a..c6567716 100644 --- a/keba/integrationpluginkeba.json +++ b/keba/integrationpluginkeba.json @@ -11,20 +11,47 @@ { "id": "900dacec-cae7-4a37-95ba-501846368ea2", "name": "wallbox", - "displayName": "Keba KeContact P30", - "createMethods": ["user"], - "interfaces": [], + "displayName": "Keba KeContact", + "createMethods": ["discovery", "user"], + "interfaces": ["extendedevcharger", "smartmeterconsumer", "connectable"], "paramTypes":[ { "id": "730cd3d3-5f0e-4028-a8c2-ced7574f13f3", - "name": "ip", - "displayName": "IP Address", + "name": "ipAddress", + "displayName": "IPv4 Address", "type": "QString", "inputType": "IPv4Address", "defaultValue":"0.0.0.0" + }, + { + "id": "c2df921d-ff8b-411c-9b1d-04a437d7dfa6", + "name": "macAddress", + "displayName": "MAC Address", + "type": "QString", + "inputType": "TextLine", + "defaultValue":"" } ], "stateTypes": [ + { + "id": "ce813458-d7d8-4f40-9648-dba4c41e92f0", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connection changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "83ed0774-2a91-434d-b03c-d920d02f2981", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set Power", + "type": "bool", + "writable": true, + "defaultValue": false + }, { "id": "539e5602-6dd9-465d-9705-3bb59bcf8982", "name": "activity", @@ -52,43 +79,133 @@ }, { "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, + "name": "maxChargingCurrent", + "displayName": "Maximal charging current", + "displayNameEvent": "Maximal charging current changed", + "displayNameAction": "Set maximal charging current", + "type": "uint", + "unit": "MilliAmpere", + "defaultValue": 6000, + "minValue": 6000, + "maxValue": 63000, "writable": true }, - { - "id": "e8f069ca-7fa7-4568-8d4c-165f6d048720", - "name": "power", - "displayName": "Power", - "displayNameEvent": "Power changed", + { + "id": "3c7b83a0-0e42-47bf-9788-dde6aab5ceea", + "name": "maxChargingCurrentPercent", + "displayName": "Maximal charging current in Percent", + "displayNameEvent": "Maximal charging current percentage changed", + "type": "uint", + "unit": "Percentage", + "defaultValue": 100, + "minValue": 0, + "maxValue": 100 + }, + { + "id": "4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9", + "name": "U1", + "displayName": "Voltage phase 1", + "displayNameEvent": "Voltage phase 1 changed", "type": "int", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "c8344ca5-21ac-4cd1-8f4b-e5ed202c5862", + "name": "U2", + "displayName": "Voltage Phase 2", + "displayNameEvent": "Voltage phase 2 changed", + "type": "int", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "5f01e86c-0943-4849-a01a-db441916ebd5", + "name": "U3", + "displayName": "Voltage Phase 3", + "displayNameEvent": "Voltage phase 3 changed", + "type": "int", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "31ec17b0-11e3-4332-92b0-fea821cf024f", + "name": "I1", + "displayName": "Current Phase 1", + "displayNameEvent": "Current phase 1 changed", + "type": "int", + "unit": "MilliAmpere", + "defaultValue": 0 + }, + { + "id": "cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97", + "name": "I2", + "displayName": "Current Phase 2", + "displayNameEvent": "Current phase 2 changed", + "type": "int", + "unit": "MilliAmpere", + "defaultValue": 0 + }, + { + "id": "da838dc8-85f0-4e55-b4b5-cb93a43b373d", + "name": "I3", + "displayName": "Current Phase 3", + "displayNameEvent": "Current phase 3 changed", + "type": "int", + "unit": "MilliAmpere", + "defaultValue": 0 + }, + { + "id": "7af9e93b-099d-4d9d-a480-9c0f66aecd8b", + "name": "P", + "displayName": "Power consumption", + "displayNameEvent": "Power consumtion changed", + "type": "int", + "unit": "MilliWatt", + "defaultValue": 0 + }, + { + "id": "a6f35ea0-aaea-438b-b818-6d161762611e", + "name": "sessionTime", + "displayName": "Session time", + "displayNameEvent": "Session time changed", + "type": "int", + "unit": "Minutes", + "defaultValue": 0 + }, + { + "id": "8e277efe-21ef-4536-bfc0-901b32d44d7c", + "name": "EP", + "displayName": "Present energy", + "displayNameEvent": "Present energy changed", + "type": "double", "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": "41e179b3-29a2-43ec-b537-023a527081e8", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumption changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + } + ], + "actionTypes": [ { - "id": "b1a574a6-46b6-44ea-a0bb-9b4de3198967", - "name": "reachable", - "displayName": "reachable", - "displayNameEvent": "Device Reachable changed", - "type": "bool", - "defaultValue": false + "id": "158b1a8f-fde9-4191-bf42-4ece5fe582e6", + "name": "display", + "displayName": "Display", + "paramTypes": [ + { + "id": "4e69a761-f4f1-42d0-83db-380894a86ebc", + "name": "message", + "displayName": "Display message", + "type": "QString", + "defaultValue": "" + } + ] } ] } @@ -96,3 +213,4 @@ } ] } + diff --git a/keba/keba.pro b/keba/keba.pro index f2871341..72cd6d1a 100644 --- a/keba/keba.pro +++ b/keba/keba.pro @@ -6,6 +6,12 @@ TARGET = $$qtLibraryTarget(nymea_integrationpluginkeba) SOURCES += \ integrationpluginkeba.cpp \ + kecontact.cpp \ + discovery.cpp \ + host.cpp \ HEADERS += \ integrationpluginkeba.h \ + kecontact.h \ + discovery.h \ + host.h \ diff --git a/keba/kecontact.cpp b/keba/kecontact.cpp new file mode 100644 index 00000000..3477ec72 --- /dev/null +++ b/keba/kecontact.cpp @@ -0,0 +1,360 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "kecontact.h" +#include "extern-plugininfo.h" + +#include + + +KeContact::KeContact(QHostAddress address, QObject *parent) : + QObject(parent), + m_address(address) +{ + m_requestTimeoutTimer = new QTimer(this); + m_requestTimeoutTimer->setSingleShot(true); + connect(m_requestTimeoutTimer, &QTimer::timeout, this, [this] { + //This timer will be started when a request is sent and stopped or resetted when a response has been received + emit connectionChanged(false); + //Try to send the next command + handleNextCommandInQueue(); + m_deviceBlocked = false; + }); +} + +KeContact::~KeContact() { + qCDebug(dcKebaKeContact()) << "Deleting KeContact connection for address" << m_address; +} + +bool KeContact::init(){ + + if(!m_udpSocket){ + m_udpSocket = new QUdpSocket(this); + if (!m_udpSocket->bind(QHostAddress::AnyIPv4, 7090, QAbstractSocket::ShareAddress)) { + qCWarning(dcKebaKeContact()) << "Cannot bind to port" << 7090; + delete m_udpSocket; + return false; + } + connect(m_udpSocket, &QUdpSocket::readyRead, this, &KeContact::readPendingDatagrams); + } + return true; +} + +QHostAddress KeContact::address() +{ + return m_address; +} + +void KeContact::setAddress(QHostAddress address) +{ + m_address = address; +} + +void KeContact::sendCommand(const QByteArray &command) +{ + if (!m_udpSocket) { + qCWarning(dcKebaKeContact()) << "UDP socket not initialized"; + emit connectionChanged(false); + return; + } + if(m_deviceBlocked) { + //add command to queue + m_commandList.append(command); + } else { + //send command + m_udpSocket->writeDatagram(command, m_address, 7090); + m_requestTimeoutTimer->start(5000); + m_deviceBlocked = true; + } +} + +void KeContact::handleNextCommandInQueue() +{ + if (!m_udpSocket) { + qCWarning(dcKebaKeContact()) << "UDP socket not initialized"; + emit connectionChanged(false); + return; + } + qCDebug(dcKebaKeContact()) << "Handle Command Queue- Pending commands" << m_commandList.length() << "Pending requestIds" << m_pendingRequests.length(); + if (!m_commandList.isEmpty()) { + QByteArray command = m_commandList.takeFirst(); + m_udpSocket->writeDatagram(command, m_address, 7090); + m_requestTimeoutTimer->start(5000); + } else { + //nothing to do + } +} + + +QUuid KeContact::enableOutput(bool state) +{ + QUuid requestId = QUuid::createUuid(); + m_pendingRequests.append(requestId); + // Print information that we are executing now the update action; + QByteArray datagram; + if(state){ + datagram.append("ena 1"); + } else{ + datagram.append("ena 0"); + } + qCDebug(dcKebaKeContact()) << "Datagram : " << datagram; + sendCommand(datagram); + QTimer::singleShot(5000, this, [requestId, this] { + if (m_pendingRequests.contains(requestId)) { + m_pendingRequests.removeOne(requestId); + emit commandExecuted(requestId, false); + } + }); + return requestId; +} + +QUuid KeContact::setMaxAmpere(int milliAmpere) +{ + QUuid requestId = QUuid::createUuid(); + m_pendingRequests.append(requestId); + // Print information that we are executing now the update action + qCDebug(dcKebaKeContact()) << "Update max current to : " << milliAmpere; + QByteArray data; + data.append("curr " + QVariant(milliAmpere).toByteArray()); + qCDebug(dcKebaKeContact()) << "send command: " << data; + sendCommand(data); + if (m_pendingRequests.contains(requestId)) { + m_pendingRequests.removeOne(requestId); + emit commandExecuted(requestId, false); + } + return requestId; +} + +QUuid KeContact::displayMessage(const QByteArray &message) +{ + /* Text shown on the display. Maximum 23 ASCII characters can be used. 0 .. 23 characters + ~ == Σ + $ == blank + , == comma + */ + QUuid requestId = QUuid::createUuid(); + m_pendingRequests.append(requestId); + qCDebug(dcKebaKeContact()) << "Set display message: " << message; + QByteArray data; + QByteArray modifiedMessage = message; + modifiedMessage.replace(" ", "$"); + if (modifiedMessage.size() > 23) { + modifiedMessage.resize(23); + } + data.append("display 0 0 0 0 " + modifiedMessage); + qCDebug(dcKebaKeContact()) << "send command: " << data; + sendCommand(data); + if (m_pendingRequests.contains(requestId)) { + m_pendingRequests.removeOne(requestId); + emit commandExecuted(requestId, false); + } + return requestId; +} + + +void KeContact::getDeviceInformation() +{ + QByteArray data; + data.append("i"); + qCDebug(dcKebaKeContact()) << "send command: " << data; + sendCommand(data); +} + +void KeContact::getReport1() +{ + QByteArray data; + data.append("report 1"); + qCDebug(dcKebaKeContact()) << "send command : " << data; + sendCommand(data); +} + +void KeContact::getReport2() +{ + QByteArray data; + data.append("report 2"); + qCDebug(dcKebaKeContact()) << "send command: " << data; + sendCommand(data); +} + +void KeContact::getReport3() +{ + QByteArray data; + data.append("report 3"); + qCDebug(dcKebaKeContact()) << "data: " << data; + sendCommand(data); +} + +QUuid KeContact::unlockCharger() +{ + QUuid requestId = QUuid::createUuid(); + m_pendingRequests.append(requestId); + QByteArray data; + data.append("unlock"); + qCDebug(dcKebaKeContact()) << "send command: " << data; + sendCommand(data); + return requestId; +} + +void KeContact::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); + if (sender != m_address) { + //Only process data from the target device + continue; + } + emit connectionChanged(true); + + qCDebug(dcKebaKeContact()) << "Data received" << datagram; + if(datagram.contains("TCH-OK")){ + + //Command response has been received, now send the next command + m_deviceBlocked = false; + m_requestTimeoutTimer->stop(); + handleNextCommandInQueue(); + + if (!m_pendingRequests.isEmpty()) { + QUuid requestId = m_pendingRequests.takeFirst(); + if (datagram.contains("done")) { + emit commandExecuted(requestId, true); + } else { + emit commandExecuted(requestId, false); + } + } else { + //Probably the response has taken too long and the requestId has been already removed + } + } else if(datagram.left(8).contains("Firmware")){ + + //Command response has been received, now send the next command + m_deviceBlocked = false; + m_requestTimeoutTimer->stop(); + handleNextCommandInQueue(); + + qCDebug(dcKebaKeContact()) << "Firmware information reveiced"; + QByteArrayList firmware = datagram.split(':'); + if (firmware.length() >= 2) { + emit deviceInformationReceived(firmware[1]); + } + } else { + + //Command response has been received, now send the next command + m_deviceBlocked = false; + m_requestTimeoutTimer->stop(); + handleNextCommandInQueue(); + + // 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(); + } + + QVariantMap data = jsonDoc.toVariant().toMap(); + + if(data.contains("ID")) { + + if (data.value("ID").toString() == "1") { + ReportOne reportOne; + qCDebug(dcKebaKeContact()) << "Report 1 received"; + reportOne.product = data.value("Product").toString(); + reportOne.firmware = data.value("Firmware").toString(); + reportOne.serialNumber = data.value("Serial").toString();; + emit reportOneReceived(reportOne); + + } else if(data.value("ID").toString() == "2"){ + + ReportTwo reportTwo; + qCDebug(dcKebaKeContact()) << "Report 2 reveiced"; + int state = data.value("State").toInt(); + reportTwo.state = State(state); + reportTwo.error1 = data.value("Error1").toInt(); + reportTwo.error2 = data.value("Error2").toInt(); + reportTwo.plugState = PlugState(data.value("Plug").toInt()); + reportTwo.enableUser = data.value("Enable user").toBool(); + reportTwo.enableSys = data.value("Enable sys").toBool(); + reportTwo.MaxCurrent = data.value("Max curr").toInt()/1000; + reportTwo.MaxCurrentPercentage = data.value("Max curr %").toInt()/10; + reportTwo.CurrentHardwareLimitation = data.value("Curr HW").toInt()/1000; + reportTwo.CurrentUser = data.value("Curr user").toInt(); + reportTwo.CurrFS = data.value("Curr FS").toInt(); + reportTwo.TmoFS = data.value("Tmo FS").toInt(); + reportTwo.output = data.value("Output").toInt(); + reportTwo.input= data.value("Input").toInt(); + reportTwo.serialNumber = data.value("Serial").toString(); + reportTwo.seconds = data.value("Sec").toInt(); + emit reportTwoReceived(reportTwo); + + } else if(data.value("ID").toString() == "3"){ + + ReportThree reportThree; + qCDebug(dcKebaKeContact()) << "Report 3 reveiced"; + reportThree.CurrentPhase1 = data.value("I1").toInt(); + reportThree.CurrentPhase2 = data.value("I2").toInt(); + reportThree.CurrentPhase3 = data.value("I3").toInt(); + reportThree.VoltagePhase1 = data.value("U1").toInt(); + reportThree.VoltagePhase2 = data.value("U2").toInt(); + reportThree.VoltagePhase3 = data.value("U3").toInt(); + reportThree.Power = data.value("P").toInt(); + reportThree.PowerFactor = data.value("PF").toInt()/10; + reportThree.EnergySession = data.value("E pres").toInt()/10000.00; + reportThree.EnergyTotal = data.value("E total").toInt()/10000.00; + reportThree.SerialNumber = data.value("Serial").toString(); + emit reportThreeReceived(reportThree); + } + } else { + if (data.contains("State")) { + emit broadcastReceived(BroadcastType::BroadcastTypeState, data.value("State")); + } + if (data.contains("Plug")) { + emit broadcastReceived(BroadcastType::BroadcastTypePlug, data.value("Plug")); + } + if (data.contains("Input")) { + emit broadcastReceived(BroadcastType::BroadcastTypeInput, data.value("Input")); + } + if (data.contains("Enable sys")) { + emit broadcastReceived(BroadcastType::BroadcastTypeEnableSys, data.value("Enable sys")); + } + if (data.contains("Max curr")) { + emit broadcastReceived(BroadcastType::BroadcastTypeMaxCurr, data.value("Max curr")); + } + if (data.contains("E pres")) { + emit broadcastReceived(BroadcastType::BroadcastTypeEPres, data.value("E pres")); + } + } + } + } +} diff --git a/keba/kecontact.h b/keba/kecontact.h new file mode 100644 index 00000000..20bd3ce6 --- /dev/null +++ b/keba/kecontact.h @@ -0,0 +1,157 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef KECONTACT_H +#define KECONTACT_H + +#include +#include +#include +#include +#include +#include + +class KeContact : public QObject +{ + Q_OBJECT +public: + explicit KeContact(QHostAddress address, QObject *parent = nullptr); + ~KeContact(); + bool init(); + + enum State { + StateStarting = 0, + StateNotReady, + StateReady, + StateCharging, + StateError, + StateAuthorizationRejected + }; + + enum PlugState { + PlugStateUnplugged = 0, + PlugStatePluggedOnChargingStation = 1, + PlugStatePluggedOnChargingStationAndPlugLocked = 3, + PlugStatePluggedOnChargingStationAndPluggedOnEV = 5, + PlugStatePluggedOnChargingStationAndPlugLockedAndPluggedOnEV = 7 + }; + + enum BroadcastType { + BroadcastTypeState = 0, + BroadcastTypePlug, + BroadcastTypeInput, + BroadcastTypeEnableSys, + BroadcastTypeMaxCurr, + BroadcastTypeEPres + }; + + struct ReportOne { + QString product; + QString serialNumber; + QString firmware; + }; + + struct ReportTwo { + State state; //Current state of the charging station + int error1; //Detail code for state 4; exceptions see FAQ on www.kecontact.com + int error2; //Detail code for state 4 exception #6 see FAQ on www.kecontact.com + PlugState plugState; //Current condition of the loading connection + bool enableSys; //Enable state for charging (contains Enable input, RFID, UDP,..). + bool enableUser; //Enable condition via UDP. + int MaxCurrent; //Current preset value via Control pilot in milliampere. + int MaxCurrentPercentage; //Current preset value via Control pilot in 0,1% of the PWM value + int CurrentHardwareLimitation; //Highest possible charging current of the charging connection. Contains device maximum, DIP-switch setting, cable coding and temperature reduction. + int CurrentUser; //Current preset value of the user via UDP; Default = 63000mA. + int CurrFS; //Current preset value for the Failsafe function. + int TmoFS; //Communication timeout before triggering the Failsafe function. + bool output; //State of the output X2. + bool input; //State of the potential free Enable input X1. When using the input, please pay attention to the information in the installation manual. + QString serialNumber; // + int seconds; //Current system clock since restart of the charging station. + }; + + struct ReportThree { + int VoltagePhase1; //voltage in V + int VoltagePhase2; //voltage in V + int VoltagePhase3; //voltage in V + int CurrentPhase1; //current in mA + int CurrentPhase2; //current in mA + int CurrentPhase3; //current in mA + int Power; //Current power in mW (Real Power). + int PowerFactor; //Power factor in 0,1% (cosphi) + int EnergySession; //Power consumption of the current loading session in 0,1Wh; Reset with new loading session (state = 2). + int EnergyTotal; //Total power consumption (persistent) without current loading session 0,1Wh; Is summed up after each completed charging session (state = 0). + QString SerialNumber; + }; + + QHostAddress address(); + int serialNumber(); + + void setAddress(QHostAddress address); + + QUuid enableOutput(bool state); + QUuid setMaxAmpere(int milliAmpere); + QUuid unlockCharger(); + QUuid displayMessage(const QByteArray &message); + + void getDeviceInformation(); + void getReport1(); + void getReport2(); + void getReport3(); + +private: + QUdpSocket *m_udpSocket = nullptr; + QHostAddress m_address; + QByteArrayList m_commandList; + bool m_deviceBlocked = false; + + QTimer *m_requestTimeoutTimer = nullptr; + int m_serialNumber; + QList m_pendingRequests; + + void sendCommand(const QByteArray &data); + void handleNextCommandInQueue(); + +signals: + void connectionChanged(bool status); + void commandExecuted(QUuid requestId, bool success); + void deviceInformationReceived(const QString &firmware); + void reportOneReceived(const ReportOne &reportOne); + void reportTwoReceived(const ReportTwo &reportTwo); + void reportThreeReceived(const ReportThree &reportThree); + void broadcastReceived(BroadcastType type, const QVariant &content); + +private slots: + void readPendingDatagrams(); +}; + + +#endif // KECONTACT_H + diff --git a/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts b/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts index 2614c172..5735065f 100644 --- a/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts +++ b/keba/translations/9142b09f-30a9-43d0-9ede-2f8debe075ac-en_US.ts @@ -4,68 +4,163 @@ IntegrationPluginKeba - + Error opening network port. - Error setting up thing - - - - - The provided IP adress is not valid. - Error setting up thing - - - - - Device with IP adress %1 is already added in the system. - Error setting up thing KebaKeContact - + + Keba KeContact - The name of the plugin KebaKeContact ({9142b09f-30a9-43d0-9ede-2f8debe075ac}) + The name of the ThingClass ({900dacec-cae7-4a37-95ba-501846368ea2}) +---------- +The name of the plugin KebaKeContact ({9142b09f-30a9-43d0-9ede-2f8debe075ac}) - + Keba The name of the vendor ({f7cda40b-829a-4675-abaa-485697430f5f}) - - Keba KeContact P30 - The name of the ThingClass ({900dacec-cae7-4a37-95ba-501846368ea2}) - - - - - IP Address - The name of the ParamType (ThingClass: wallbox, Type: thing, ID: {730cd3d3-5f0e-4028-a8c2-ced7574f13f3}) - - - - + Activity changed The name of the EventType ({539e5602-6dd9-465d-9705-3bb59bcf8982}) of ThingClass wallbox - - + + Activity The name of the ParamType (ThingClass: wallbox, EventType: activity, ID: {539e5602-6dd9-465d-9705-3bb59bcf8982}) ---------- The name of the StateType ({539e5602-6dd9-465d-9705-3bb59bcf8982}) of ThingClass wallbox + + + + Connected + The name of the ParamType (ThingClass: wallbox, EventType: connected, ID: {ce813458-d7d8-4f40-9648-dba4c41e92f0}) +---------- +The name of the StateType ({ce813458-d7d8-4f40-9648-dba4c41e92f0}) of ThingClass wallbox + + - + Connection changed + The name of the EventType ({ce813458-d7d8-4f40-9648-dba4c41e92f0}) of ThingClass wallbox + + + + + + Current Phase 1 + The name of the ParamType (ThingClass: wallbox, EventType: I1, ID: {31ec17b0-11e3-4332-92b0-fea821cf024f}) +---------- +The name of the StateType ({31ec17b0-11e3-4332-92b0-fea821cf024f}) of ThingClass wallbox + + + + + + Current Phase 2 + The name of the ParamType (ThingClass: wallbox, EventType: I2, ID: {cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97}) +---------- +The name of the StateType ({cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97}) of ThingClass wallbox + + + + + + Current Phase 3 + The name of the ParamType (ThingClass: wallbox, EventType: I3, ID: {da838dc8-85f0-4e55-b4b5-cb93a43b373d}) +---------- +The name of the StateType ({da838dc8-85f0-4e55-b4b5-cb93a43b373d}) of ThingClass wallbox + + + + + Current phase 1 changed + The name of the EventType ({31ec17b0-11e3-4332-92b0-fea821cf024f}) of ThingClass wallbox + + + + + Current phase 2 changed + The name of the EventType ({cdc7e10a-0d0a-4e93-ad2c-d34ffca45c97}) of ThingClass wallbox + + + + + Current phase 3 changed + The name of the EventType ({da838dc8-85f0-4e55-b4b5-cb93a43b373d}) of ThingClass wallbox + + + + + Display + The name of the ActionType ({158b1a8f-fde9-4191-bf42-4ece5fe582e6}) of ThingClass wallbox + + + + + Display message + The name of the ParamType (ThingClass: wallbox, ActionType: display, ID: {4e69a761-f4f1-42d0-83db-380894a86ebc}) + + + + + IPv4 Address + The name of the ParamType (ThingClass: wallbox, Type: thing, ID: {730cd3d3-5f0e-4028-a8c2-ced7574f13f3}) + + + + + MAC Address + The name of the ParamType (ThingClass: wallbox, Type: thing, ID: {c2df921d-ff8b-411c-9b1d-04a437d7dfa6}) + + + + + + + Maximal charging current + The name of the ParamType (ThingClass: wallbox, ActionType: maxChargingCurrent, ID: {593656f0-babf-4308-8767-68f34e10fb15}) +---------- +The name of the ParamType (ThingClass: wallbox, EventType: maxChargingCurrent, ID: {593656f0-babf-4308-8767-68f34e10fb15}) +---------- +The name of the StateType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass wallbox + + + + + Maximal charging current changed + The name of the EventType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass wallbox + + + + + + Maximal charging current in Percent + The name of the ParamType (ThingClass: wallbox, EventType: maxChargingCurrentPercent, ID: {3c7b83a0-0e42-47bf-9788-dde6aab5ceea}) +---------- +The name of the StateType ({3c7b83a0-0e42-47bf-9788-dde6aab5ceea}) of ThingClass wallbox + + + + + Maximal charging current percentage changed + The name of the EventType ({3c7b83a0-0e42-47bf-9788-dde6aab5ceea}) of ThingClass wallbox + + + + + Plug State The name of the ParamType (ThingClass: wallbox, EventType: plugState, ID: {3b4d29f3-3101-47ad-90fd-269b6348783b}) ---------- @@ -73,8 +168,125 @@ The name of the StateType ({3b4d29f3-3101-47ad-90fd-269b6348783b}) of ThingClass - - + + + Power consumption + The name of the ParamType (ThingClass: wallbox, EventType: P, ID: {7af9e93b-099d-4d9d-a480-9c0f66aecd8b}) +---------- +The name of the StateType ({7af9e93b-099d-4d9d-a480-9c0f66aecd8b}) of ThingClass wallbox + + + + + Power consumtion changed + The name of the EventType ({7af9e93b-099d-4d9d-a480-9c0f66aecd8b}) of ThingClass wallbox + + + + + + Present energy + The name of the ParamType (ThingClass: wallbox, EventType: EP, ID: {8e277efe-21ef-4536-bfc0-901b32d44d7c}) +---------- +The name of the StateType ({8e277efe-21ef-4536-bfc0-901b32d44d7c}) of ThingClass wallbox + + + + + Present energy changed + The name of the EventType ({8e277efe-21ef-4536-bfc0-901b32d44d7c}) of ThingClass wallbox + + + + + + Session time + The name of the ParamType (ThingClass: wallbox, EventType: sessionTime, ID: {a6f35ea0-aaea-438b-b818-6d161762611e}) +---------- +The name of the StateType ({a6f35ea0-aaea-438b-b818-6d161762611e}) of ThingClass wallbox + + + + + Session time changed + The name of the EventType ({a6f35ea0-aaea-438b-b818-6d161762611e}) of ThingClass wallbox + + + + + Set Power + The name of the ActionType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass wallbox + + + + + Set maximal charging current + The name of the ActionType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass wallbox + + + + + + Total energy consumed + The name of the ParamType (ThingClass: wallbox, EventType: totalEnergyConsumed, ID: {41e179b3-29a2-43ec-b537-023a527081e8}) +---------- +The name of the StateType ({41e179b3-29a2-43ec-b537-023a527081e8}) of ThingClass wallbox + + + + + Total energy consumption changed + The name of the EventType ({41e179b3-29a2-43ec-b537-023a527081e8}) of ThingClass wallbox + + + + + + Voltage Phase 2 + The name of the ParamType (ThingClass: wallbox, EventType: U2, ID: {c8344ca5-21ac-4cd1-8f4b-e5ed202c5862}) +---------- +The name of the StateType ({c8344ca5-21ac-4cd1-8f4b-e5ed202c5862}) of ThingClass wallbox + + + + + + Voltage Phase 3 + The name of the ParamType (ThingClass: wallbox, EventType: U3, ID: {5f01e86c-0943-4849-a01a-db441916ebd5}) +---------- +The name of the StateType ({5f01e86c-0943-4849-a01a-db441916ebd5}) of ThingClass wallbox + + + + + + Voltage phase 1 + The name of the ParamType (ThingClass: wallbox, EventType: U1, ID: {4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9}) +---------- +The name of the StateType ({4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9}) of ThingClass wallbox + + + + + Voltage phase 1 changed + The name of the EventType ({4a2d75d8-a3a0-4b40-9ca7-e8b6f11d0ef9}) of ThingClass wallbox + + + + + Voltage phase 2 changed + The name of the EventType ({c8344ca5-21ac-4cd1-8f4b-e5ed202c5862}) of ThingClass wallbox + + + + + Voltage phase 3 changed + The name of the EventType ({5f01e86c-0943-4849-a01a-db441916ebd5}) of ThingClass wallbox + + + + + Current The name of the ParamType (ThingClass: wallbox, EventType: current, ID: {a29c1748-fe97-4830-a56e-e1cc4e618385}) ---------- @@ -82,93 +294,33 @@ The name of the StateType ({a29c1748-fe97-4830-a56e-e1cc4e618385}) of ThingClass - - - - maximal Current - The name of the ParamType (ThingClass: wallbox, ActionType: maxCurrent, ID: {593656f0-babf-4308-8767-68f34e10fb15}) ----------- -The name of the ParamType (ThingClass: wallbox, EventType: maxCurrent, ID: {593656f0-babf-4308-8767-68f34e10fb15}) ----------- -The name of the StateType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass wallbox - - - - - + + + Power - The name of the ParamType (ThingClass: wallbox, EventType: power, ID: {e8f069ca-7fa7-4568-8d4c-165f6d048720}) + The name of the ParamType (ThingClass: wallbox, ActionType: power, ID: {83ed0774-2a91-434d-b03c-d920d02f2981}) ---------- -The name of the StateType ({e8f069ca-7fa7-4568-8d4c-165f6d048720}) of ThingClass wallbox +The name of the ParamType (ThingClass: wallbox, EventType: power, ID: {83ed0774-2a91-434d-b03c-d920d02f2981}) +---------- +The name of the StateType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass wallbox - - - - Output - The name of the ParamType (ThingClass: wallbox, ActionType: outEnable, ID: {0cd5396a-bc41-4c8f-b037-db10991a76c7}) ----------- -The name of the ParamType (ThingClass: wallbox, EventType: outEnable, ID: {0cd5396a-bc41-4c8f-b037-db10991a76c7}) ----------- -The name of the StateType ({0cd5396a-bc41-4c8f-b037-db10991a76c7}) of ThingClass wallbox - - - - + Plug State changed The name of the EventType ({3b4d29f3-3101-47ad-90fd-269b6348783b}) of ThingClass wallbox - + Current changed The name of the EventType ({a29c1748-fe97-4830-a56e-e1cc4e618385}) of ThingClass wallbox - - Maximal Current changed - The name of the EventType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass wallbox - - - - - Set maximal current - The name of the ActionType ({593656f0-babf-4308-8767-68f34e10fb15}) of ThingClass wallbox - - - - + Power changed - The name of the EventType ({e8f069ca-7fa7-4568-8d4c-165f6d048720}) of ThingClass wallbox - - - - - Output Enable changed - The name of the EventType ({0cd5396a-bc41-4c8f-b037-db10991a76c7}) of ThingClass wallbox - - - - - Set Output - The name of the ActionType ({0cd5396a-bc41-4c8f-b037-db10991a76c7}) of ThingClass wallbox - - - - - Device Reachable changed - The name of the EventType ({b1a574a6-46b6-44ea-a0bb-9b4de3198967}) of ThingClass wallbox - - - - - - reachable - The name of the ParamType (ThingClass: wallbox, EventType: reachable, ID: {b1a574a6-46b6-44ea-a0bb-9b4de3198967}) ----------- -The name of the StateType ({b1a574a6-46b6-44ea-a0bb-9b4de3198967}) of ThingClass wallbox + The name of the EventType ({83ed0774-2a91-434d-b03c-d920d02f2981}) of ThingClass wallbox